Compare commits

..

7 commits

94 changed files with 10072 additions and 4216 deletions

View file

@ -61,7 +61,7 @@
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "2kb", "maximumWarning": "2kb",
"maximumError": "6kb" "maximumError": "10kb"
} }
], ],
"outputHashing": "all" "outputHashing": "all"

View file

@ -1,6 +1,6 @@
{ {
"name": "MikroWizard", "name": "MikroWizard",
"version": "1.2.0", "version": "1.3.0",
"copyright": "MikroWizard mikrowizard.com", "copyright": "MikroWizard mikrowizard.com",
"license": "AGPL", "license": "AGPL",
"author": "MikroWizard Team (https://github.com/MikroWizard)", "author": "MikroWizard Team (https://github.com/MikroWizard)",
@ -19,17 +19,17 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.3.5", "@angular/animations": "^18.2.14",
"@angular/cdk": "^16.2.9", "@angular/cdk": "^16.2.9",
"@angular/common": "^17.3.5", "@angular/common": "^18.2.14",
"@angular/compiler": "^17.3.5", "@angular/compiler": "^18.2.14",
"@angular/core": "^17.3.5", "@angular/core": "^18.2.14",
"@angular/forms": "^17.3.5", "@angular/forms": "^18.2.14",
"@angular/language-service": "^17.3.5", "@angular/language-service": "^18.2.14",
"@angular/material": "^17.3.5", "@angular/material": "^17.3.5",
"@angular/platform-browser": "^17.3.5", "@angular/platform-browser": "^18.2.14",
"@angular/platform-browser-dynamic": "^17.3.5", "@angular/platform-browser-dynamic": "^18.2.14",
"@angular/router": "^17.3.5", "@angular/router": "^18.2.14",
"@coreui/angular": "~4.5.27", "@coreui/angular": "~4.5.27",
"@coreui/angular-chartjs": "~4.5.27", "@coreui/angular-chartjs": "~4.5.27",
"@coreui/chartjs": "^3.1.2", "@coreui/chartjs": "^3.1.2",
@ -42,6 +42,7 @@
"@generic-ui/fabric": "^0.19.0", "@generic-ui/fabric": "^0.19.0",
"@generic-ui/hermes": "^0.19.0", "@generic-ui/hermes": "^0.19.0",
"@generic-ui/ngx-grid": "^0.19.0", "@generic-ui/ngx-grid": "^0.19.0",
"@primeuix/themes": "^2.0.3",
"chart.js": "^3.9.1", "chart.js": "^3.9.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"date-fns-jalali": "^3.6.0-0", "date-fns-jalali": "^3.6.0-0",
@ -61,17 +62,21 @@
"ngx-material-date-fns-adapter": "^18.0.0", "ngx-material-date-fns-adapter": "^18.0.0",
"ngx-scrollbar": "^13.0.3", "ngx-scrollbar": "^13.0.3",
"ngx-super-select": "^3.17.0", "ngx-super-select": "^3.17.0",
"primeicons": "^7.0.0",
"primeng": "^18.0.2",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"vis-data": "^7.1.9", "vis-data": "^7.1.9",
"vis-network": "^9.1.9", "vis-network": "^9.1.9",
"zone.js": "~0.14.4" "vis-util": "^6.0.0",
"zone.js": "~0.14.10"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.3.5", "@angular-devkit/build-angular": "^18.2.21",
"@angular/cli": "^17.3.5", "@angular/cli": "^18.2.21",
"@angular/compiler-cli": "^17.3.5", "@angular/compiler-cli": "^18.2.14",
"@angular/localize": "^17.3.5", "@angular/localize": "^18.2.14",
"@types/hammerjs": "^2.0.46",
"@types/jasmine": "^5.1.1", "@types/jasmine": "^5.1.1",
"@types/lodash-es": "^4.17.10", "@types/lodash-es": "^4.17.10",
"@types/node": "^18.19.34", "@types/node": "^18.19.34",

View file

@ -69,6 +69,11 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./views/syslog/syslog.module').then((m) => m.SyslogModule) import('./views/syslog/syslog.module').then((m) => m.SyslogModule)
}, },
{
path: 'syslog-regex',
loadChildren: () =>
import('./views/syslog-regex/syslog-regex.module').then((m) => m.SyslogRegexModule)
},
{ {
path: 'backups', path: 'backups',
loadChildren: () => loadChildren: () =>
@ -99,6 +104,11 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./views/snippets/snippets.module').then((m) => m.SnippetsModule) import('./views/snippets/snippets.module').then((m) => m.SnippetsModule)
}, },
{
path: 'sequences',
loadChildren: () =>
import('./views/sequences/sequences.module').then((m) => m.SequencesModule)
},
{ {
path: 'user_manager', path: 'user_manager',
loadChildren: () => loadChildren: () =>
@ -109,6 +119,11 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./views/permissions/permissions.module').then((m) => m.PermissionsModule) import('./views/permissions/permissions.module').then((m) => m.PermissionsModule)
}, },
{
path: 'vpn',
loadChildren: () =>
import('./views/vpn/vpn.module').then((m) => m.VpnModule)
},
{ {
path: 'pages', path: 'pages',
loadChildren: () => loadChildren: () =>
@ -137,7 +152,7 @@ const routes: Routes = [
title: 'Login Page' title: 'Login Page'
} }
}, },
{path: '**', redirectTo: 'dashboard'} { path: '**', redirectTo: 'dashboard' }
]; ];
@NgModule({ @NgModule({

View file

@ -2,10 +2,15 @@ import { NgModule ,APP_INITIALIZER} from '@angular/core';
import { HashLocationStrategy, LocationStrategy, PathLocationStrategy } from '@angular/common'; import { HashLocationStrategy, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { BrowserModule, Title } from '@angular/platform-browser'; import { BrowserModule, Title } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeuix/themes/aura';
import { ReactiveFormsModule,FormsModule } from '@angular/forms'; import { ReactiveFormsModule,FormsModule } from '@angular/forms';
import { NgScrollbarModule } from 'ngx-scrollbar'; import { NgScrollbarModule } from 'ngx-scrollbar';
import { HttpClientModule } from '@angular/common/http'; import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
import { LicenseService } from './providers/license.service';
import { LicenseInterceptor } from './providers/license-interceptor.service';
// Import routing module // Import routing module
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
@ -54,60 +59,69 @@ const APP_CONTAINERS = [
export function loginStatusProviderFactory(provider: loginChecker) { export function loginStatusProviderFactory(provider: loginChecker) {
return () => provider.load(); return () => provider.load();
} }
@NgModule({ @NgModule({ declarations: [AppComponent, ...APP_CONTAINERS],
declarations: [AppComponent, ...APP_CONTAINERS], bootstrap: [AppComponent], imports: [BrowserModule,
imports: [ BrowserAnimationsModule,
BrowserModule, AppRoutingModule,
BrowserAnimationsModule, AvatarModule,
AppRoutingModule, BreadcrumbModule,
AvatarModule, FooterModule,
BreadcrumbModule, DropdownModule,
FooterModule, GridModule,
DropdownModule, HeaderModule,
GridModule, SidebarModule,
HeaderModule, IconModule,
SidebarModule, NavModule,
IconModule, ButtonModule,
NavModule, FormModule,
HttpClientModule, UtilitiesModule,
ButtonModule, ButtonGroupModule,
FormModule, ReactiveFormsModule,
UtilitiesModule, FormsModule,
ButtonGroupModule, SidebarModule,
ReactiveFormsModule, SharedModule,
FormsModule, TabsModule,
SidebarModule, ListGroupModule,
SharedModule, ProgressModule,
TabsModule, BadgeModule,
ListGroupModule, ListGroupModule,
ProgressModule, CardModule,
BadgeModule, NgScrollbarModule,
ListGroupModule, ModalModule,
CardModule, FontAwesomeModule,
NgScrollbarModule, TableModule], providers: [
ModalModule, {
FontAwesomeModule, provide: LocationStrategy,
TableModule useClass: HashLocationStrategy
], },
providers: [ MikroWizardProvider,
{ dataProvider,
provide: LocationStrategy, loginChecker,
useClass: HashLocationStrategy IconSetService,
}, provideDateFnsAdapter(),
MikroWizardProvider, provideAnimationsAsync(),
dataProvider, providePrimeNG({
loginChecker, theme: {
IconSetService, preset: Aura,
provideDateFnsAdapter(), options: {
{ darkModeSelector: 'none'
provide: APP_INITIALIZER, }
useFactory: loginStatusProviderFactory, }
deps: [loginChecker], }),
multi: true, {
}, provide: APP_INITIALIZER,
Title useFactory: loginStatusProviderFactory,
], deps: [loginChecker],
bootstrap: [AppComponent] multi: true,
}) },
Title,
LicenseService,
{
provide: HTTP_INTERCEPTORS,
useClass: LicenseInterceptor,
multi: true
},
provideHttpClient(withInterceptorsFromDi())
] })
export class AppModule { export class AppModule {
} }

View file

@ -10,13 +10,19 @@ export const navItems: INavData[] = [
{ {
name: 'Monitoring Wall', name: 'Monitoring Wall',
url: '/monitoring', url: '/monitoring',
icon:'fa-solid fa-tv', icon: 'fa-solid fa-tv',
attributes: { 'pro':true } attributes: { 'pro': true }
}, },
{ {
title: true, title: true,
name: 'Device Managment' name: 'Device Managment'
}, },
{
name: 'WireGuard Server',
url: '/vpn',
icon: 'fa-solid fa-network-wired',
attributes: { 'pro': true }
},
{ {
name: 'Devices', name: 'Devices',
url: '/devices', url: '/devices',
@ -31,8 +37,8 @@ export const navItems: INavData[] = [
{ {
name: 'Network Maps', name: 'Network Maps',
url: '/maps', url: '/maps',
icon:'fa-solid fa-map', icon: 'fa-solid fa-map',
attributes: { 'pro':true } attributes: { 'pro': true }
}, },
// { // {
// name: 'Tools', // name: 'Tools',
@ -70,18 +76,24 @@ export const navItems: INavData[] = [
url: '/snippets', url: '/snippets',
icon: 'fa-solid fa-code' icon: 'fa-solid fa-code'
}, },
{
name: 'Sequences',
url: '/sequences',
icon: 'fa-solid fa-code-branch',
attributes: { 'pro': true }
},
{ {
name: 'Sync and Cloner', name: 'Sync and Cloner',
url: '/cloner', url: '/cloner',
icon: 'fa-solid fa-rotate', icon: 'fa-solid fa-rotate',
attributes: { 'pro':true } attributes: { 'pro': true }
}, },
{ {
name: 'Password Vault', name: 'Password Vault',
url: '/vault', url: '/vault',
icon:'fa-solid fa-vault', icon: 'fa-solid fa-vault',
attributes: { 'pro':true } attributes: { 'pro': true }
}, },
// { // {
// name: 'Tools', // name: 'Tools',
@ -133,6 +145,12 @@ export const navItems: INavData[] = [
icon: 'fa-solid fa-person-circle-question', icon: 'fa-solid fa-person-circle-question',
}, },
{
name: 'Syslog Custom Regex',
url: '/syslog-regex',
icon: 'fa-solid fa-code-commit',
attributes: { 'pro': true }
},
{ {
title: true, title: true,
name: 'Users' name: 'Users'
@ -140,12 +158,12 @@ export const navItems: INavData[] = [
{ {
name: 'Users Management', name: 'Users Management',
url: '/user_manager', url: '/user_manager',
icon: 'fa-solid fa-user-gear' , icon: 'fa-solid fa-user-gear',
}, },
{ {
name: 'Permissions', name: 'Permissions',
url: '/permissions', url: '/permissions',
icon: 'fa-solid fa-users' , icon: 'fa-solid fa-users',
}, },
{ {
title: true, title: true,
@ -155,7 +173,7 @@ export const navItems: INavData[] = [
{ {
name: 'Settings', name: 'Settings',
url: '/settings', url: '/settings',
icon: 'fa-solid fa-gear' , icon: 'fa-solid fa-gear',
}, },
// { // {
// name: 'Backup', // name: 'Backup',
@ -177,7 +195,7 @@ export const navItems: INavData[] = [
{ {
name: 'Buy Pro', name: 'Buy Pro',
url: 'https://mikrowizard.com/pricing/', url: 'https://mikrowizard.com/pricing/',
icon:'fa-solid fa-money-check-dollar', icon: 'fa-solid fa-money-check-dollar',
attributes: { 'free':true,target: '_blank' } attributes: { 'free': true, target: '_blank' }
} }
]; ];

View file

@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpResponse
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { LicenseService } from './license.service';
@Injectable()
export class LicenseInterceptor implements HttpInterceptor {
// Pro-licensed API endpoints that should be monitored
private readonly proEndpoints = [
'monitoring/devs/get',
'monitoring/events/get',
'monitoring/eventunfixed/get',
'/api/vpn/status',
'networkmap/get',
'snippet/sequence/list',
'cloner/list',
'/api/pssvault/get',
'snippet/syslogregex/list'
];
constructor(private licenseService: LicenseService) { }
private isProEndpoint(url: string): boolean {
return this.proEndpoints.some(endpoint => url.includes(endpoint));
}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && this.isProEndpoint(request.url)) {
const body = event.body;
if (
body &&
body.result &&
typeof body.result === 'object' &&
body.result.err === 'License Expired' &&
body.result.status === 'failed'
) {
this.licenseService.setExpired(true);
} else {
// Pro endpoint responded without license error — license is valid
this.licenseService.setExpired(false);
}
}
})
);
}
}

View file

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LicenseService {
private isExpiredSubject = new BehaviorSubject<boolean>(false);
public isExpired$: Observable<boolean> = this.isExpiredSubject.asObservable();
constructor() { }
setExpired(expired: boolean): void {
if (this.isExpiredSubject.value !== expired) {
this.isExpiredSubject.next(expired);
}
}
isExpired(): boolean {
return this.isExpiredSubject.value;
}
}

View file

@ -60,60 +60,60 @@ export class dataProvider {
//// ////
//// MikroWizard API //// MikroWizard API
//// ////
get_front_version(){ get_front_version() {
return this.MikroWizardRPC.sendHttpGetRequest("/api/frontver/"); return this.MikroWizardRPC.sendHttpGetRequest("/api/frontver/");
} }
change_password(oldpass:string,newpass:string){ change_password(oldpass: string, newpass: string) {
var data={ var data = {
'oldpass':oldpass, 'oldpass': oldpass,
'newpass':newpass 'newpass': newpass
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data);
} }
dashboard_stats(versioncheck:boolean,front_version:string){ dashboard_stats(versioncheck: boolean, front_version: string) {
var data={ var data = {
'versioncheck':versioncheck, 'versioncheck': versioncheck,
'front_version':front_version 'front_version': front_version
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data); return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data);
} }
monitoring_devices_events(page:number,textfilter:string=''){ monitoring_devices_events(page: number, textfilter: string = '') {
var data={ var data = {
'page':page, 'page': page,
'textfilter':textfilter 'textfilter': textfilter
} }
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/devs/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/devs/get", data);
} }
monitoring_events_fix(event_id:number){ monitoring_events_fix(event_id: number) {
var data={ var data = {
'event_id':event_id 'event_id': event_id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/fix", data); return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/fix", data);
} }
monitoring_all_events(devid:number,page:number){ monitoring_all_events(devid: number, page: number) {
var data={ var data = {
'devid':devid, 'devid': devid,
'page':page 'page': page
} }
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/get", data);
} }
monitoring_unfixed_events(devid:number){ monitoring_unfixed_events(devid: number) {
var data={ var data = {
'devid':devid 'devid': devid
} }
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/eventunfixed/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/eventunfixed/get", data);
} }
dashboard_traffic(delta:string){ dashboard_traffic(delta: string) {
var data={ var data = {
'delta':delta 'delta': delta
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/traffic", data); return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/traffic", data);
} }
get_dev_list(data:any) { get_dev_list(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/dev/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/list", data);
} }
@ -121,376 +121,436 @@ export class dataProvider {
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/list", {}); return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/list", {});
} }
get_devgroup_members(gid:number) { get_devgroup_members(gid: number) {
var data={ var data = {
'gid':gid 'gid': gid
} }
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/members", data); return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/members", data);
} }
delete_group(id:number){ delete_group(id: number) {
var data={ var data = {
'gid':id 'gid': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/delete", data);
} }
delete_devices(devids:any){ delete_devices(devids: any) {
var data = { var data = {
'devids':devids 'devids': devids
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/delete", data);
} }
get_dev_info(id: number) { get_dev_info(id: number) {
var data={ var data = {
'devid':id 'devid': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/info", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/info", data);
} }
get_editform(id: number) { get_editform(id: number) {
var data={ var data = {
'devid':id 'devid': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/get_editform", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/get_editform", data);
} }
save_editform(data:any){ save_editform(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/dev/save_editform", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/save_editform", data);
} }
get_dev_sensors(id: number,delta:string="5m",total_type:string="bps") { get_dev_sensors(id: number, delta: string = "5m", total_type: string = "bps") {
var data={ var data = {
'devid':id, 'devid': id,
'delta':delta, 'delta': delta,
'total':total_type 'total': total_type
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/sensors", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/sensors", data);
} }
get_dev_radio_sensors(id: number, delta:string="5m"){ get_dev_radio_sensors(id: number, delta: string = "5m") {
var data={ var data = {
'devid':id, 'devid': id,
'delta':delta 'delta': delta
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data);
} }
get_dev_dhcp_info(id: number){ get_dev_dhcp_info(id: number) {
var data={ var data = {
'devid':id, 'devid': id,
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data);
} }
get_dev_ifstat(id: number,delta:string="5m",iface:string="ether1",type:string="bps") { get_dev_ifstat(id: number, delta: string = "5m", iface: string = "ether1", type: string = "bps") {
var data={ var data = {
'devid':id, 'devid': id,
'delta':delta, 'delta': delta,
'type':type, 'type': type,
'interface':iface 'interface': iface
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/ifstat", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/ifstat", data);
} }
totp(action:string,userid:string){ totp(action: string, userid: string) {
var data={ var data = {
'userid':userid, 'userid': userid,
'action':action 'action': action
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user/totp", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/totp", data);
} }
get_user_restrictions(uid:string){ get_user_restrictions(uid: string) {
var data={ var data = {
'uid':uid 'uid': uid
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user/restrictions", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/restrictions", data);
} }
save_user_restrictions(uid:string,restrictions:any){ save_user_restrictions(uid: string, restrictions: any) {
var data={ var data = {
'uid':uid, 'uid': uid,
'restrictions':restrictions 'restrictions': restrictions
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user/save_restrictions", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/save_restrictions", data);
} }
mytotp(action:string,otp:any=false){ mytotp(action: string, otp: any = false) {
var data={ var data = {
'action':action, 'action': action,
'otp':otp 'otp': otp
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user/mytotp", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/mytotp", data);
} }
get_auth_logs(filters:any) { get_auth_logs(filters: any) {
var data=filters; var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/auth/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/auth/list", data);
} }
get_account_logs(filters:any) { get_account_logs(filters: any) {
var data=filters; var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/account/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/account/list", data);
} }
get_dev_logs(filters:any) { get_dev_logs(filters: any) {
var data=filters; var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/list", data);
} }
get_syslog(filters:any) { get_syslog(filters: any) {
var data=filters; var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/syslog/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/syslog/list", data);
} }
get_details_grouped(devid:number=0){ get_details_grouped(devid: number = 0) {
var data={ var data = {
'devid':devid 'devid': devid
} }
return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/details/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/details/list", data);
} }
scan_devs(type:string,info:any){ scan_devs(type: string, info: any) {
var data: any={ var data: any = {
'type':type 'type': type
} }
if(type=="ip"){ if (type == "ip") {
data = Object.assign(data, info); data = Object.assign(data, info);
} }
return this.MikroWizardRPC.sendJsonRequest("/api/scanner/scan", data); return this.MikroWizardRPC.sendJsonRequest("/api/scanner/scan", data);
} }
scan_results(){ scan_results() {
return this.MikroWizardRPC.sendJsonRequest("/api/scanner/results", {}); return this.MikroWizardRPC.sendJsonRequest("/api/scanner/results", {});
} }
get_groups(searchstr:string=""){ get_groups(searchstr: string = "") {
var data={ var data = {
'searchstr':searchstr 'searchstr': searchstr
} }
return this.MikroWizardRPC.sendJsonRequest("/api/search/groups", data); return this.MikroWizardRPC.sendJsonRequest("/api/search/groups", data);
} }
get_devices(searchstr:string=""){ get_devices(searchstr: string = "") {
var data={ var data = {
'searchstr':searchstr 'searchstr': searchstr
} }
return this.MikroWizardRPC.sendJsonRequest("/api/search/devices", data); return this.MikroWizardRPC.sendJsonRequest("/api/search/devices", data);
} }
update_save_group(group:any){ update_save_group(group: any) {
var data={ var data = {
...group ...group
} }
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/update_save_group", data); return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/update_save_group", data);
} }
get_snippets(name:string,desc:string,content:string,page:number=0,size:number=1000,limit:any=false){ get_snippets(name: string, desc: string, content: string, page: number = 0, size: number = 1000, limit: any = false) {
var data={ var data = {
'name':name, 'name': name,
'description':desc, 'description': desc,
'content':content, 'content': content,
'page':page, 'page': page,
'size':size, 'size': size,
'limit':limit 'limit': limit
} }
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/snippet/list", data);
} }
save_snippet(data:any){ save_snippet(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", {...data}); return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", { ...data });
} }
Exec_snipet(data:any,members:any) { Exec_snipet(data: any, members: any) {
data['members']=members; data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/exec", data); return this.MikroWizardRPC.sendJsonRequest("/api/snippet/exec", data);
} }
delete_snippet(id:number){ delete_snippet(id: number) {
var data={ var data = {
'id':id 'id': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/snippet/delete", data);
} }
get_executed_snipet(id:number){ get_executed_snipet(id: number) {
var data={ var data = {
'id':id 'id': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/executed", data); return this.MikroWizardRPC.sendJsonRequest("/api/snippet/executed", data);
} }
get_sequences() {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/list", {});
}
save_sequence(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/save", { ...data });
}
delete_sequence(id: number) {
var data = {
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/delete", data);
}
get_sequence_history(id: number) {
var data = {
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/history", data);
}
exec_sequence(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/exec", data);
}
get_syslog_regexes() {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/list", {});
}
save_syslog_regex(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/save", { ...data });
}
delete_syslog_regex(id: number) {
var data = {
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/delete", data);
}
get_syslogregex_samples() {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/samples", {});
}
get_alerts() {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/list", {});
}
save_alert(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/save", { ...data });
}
delete_alert(id: number) {
var data = {
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/delete", data);
}
get_user_task_list() { get_user_task_list() {
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/list", {}); return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/list", {});
} }
Add_task(data:any,members:any) { Add_task(data: any, members: any) {
data['members']=members; data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/create", data); return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/create", data);
} }
Delete_task(taskid:Number) { Delete_task(taskid: Number) {
var data={ var data = {
'taskid':taskid, 'taskid': taskid,
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/delete", data);
} }
Edit_task(data:any,members:any) { Edit_task(data: any, members: any) {
data['members']=members; data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/edit", data); return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/edit", data);
} }
get_task_members(taskid:Number) { get_task_members(taskid: Number) {
var data={ var data = {
'taskid':taskid, 'taskid': taskid,
} }
return this.MikroWizardRPC.sendJsonRequest("/api/taskmember/details", data); return this.MikroWizardRPC.sendJsonRequest("/api/taskmember/details", data);
} }
get_users(page:Number,size:Number,search:string) { get_users(page: Number, size: Number, search: string) {
var data={ var data = {
'page':page, 'page': page,
'size':size, 'size': size,
'search':search 'search': search
} }
return this.MikroWizardRPC.sendJsonRequest("/api/users/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/users/list", data);
} }
get_perms(page:Number,size:Number,search:string) { get_perms(page: Number, size: Number, search: string) {
var data={ var data = {
'page':page, 'page': page,
'size':size, 'size': size,
'search':search 'search': search
} }
return this.MikroWizardRPC.sendJsonRequest("/api/perms/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/perms/list", data);
} }
create_perm(name:string,perms:any) { create_perm(name: string, perms: any) {
var data={ var data = {
'name':name, 'name': name,
'perms':perms 'perms': perms
} }
return this.MikroWizardRPC.sendJsonRequest("/api/perms/create", data); return this.MikroWizardRPC.sendJsonRequest("/api/perms/create", data);
} }
edit_perm(id:Number,name:string,perms:any) { edit_perm(id: Number, name: string, perms: any) {
var data = { var data = {
'id':id, 'id': id,
'name':name, 'name': name,
'perms':perms 'perms': perms
} }
return this.MikroWizardRPC.sendJsonRequest("/api/perms/edit", data); return this.MikroWizardRPC.sendJsonRequest("/api/perms/edit", data);
} }
delete_perm(id:number){ delete_perm(id: number) {
var data={ var data = {
'id':id 'id': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/perms/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/perms/delete", data);
} }
get_vault_setting(){ get_vault_setting() {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get", {}); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get", {});
} }
vault_task(data:any){ vault_task(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/task", data); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/task", data);
} }
vault_history(){ vault_history() {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/history", {}); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/history", {});
} }
exec_vault(){ exec_vault() {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/execute", {}); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/execute", {});
} }
reveal_password(devid:number,username:string){ reveal_password(devid: number, username: string) {
var data={ var data = {
'devid':devid, 'devid': devid,
'username':username 'username': username
} }
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/reveal", data); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/reveal", data);
} }
get_passwords(data:any){ get_passwords(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_passwords", data); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_passwords", data);
} }
get_device_pass(devid:number){ get_device_pass(devid: number) {
var data={ var data = {
'devid':devid 'devid': devid
} }
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_device_pass", data); return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_device_pass", data);
} }
user_perms(uid:string) { user_perms(uid: string) {
var data = { var data = {
'uid':uid, 'uid': uid,
} }
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/userperms/list", data);
} }
Add_user_perm(uid:Number,permid:Number,devgroupid:Number){ Add_user_perm(uid: Number, permid: Number, devgroupid: Number) {
var data = { var data = {
'uid':uid, 'uid': uid,
'pid':permid, 'pid': permid,
'gid':devgroupid 'gid': devgroupid
} }
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/create", data); return this.MikroWizardRPC.sendJsonRequest("/api/userperms/create", data);
} }
Delete_user_perm(id:number){ Delete_user_perm(id: number) {
var data={ var data = {
'id':id 'id': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/userperms/delete", data);
} }
edit_user(data:any) { edit_user(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/user/edit", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/edit", data);
} }
create_user(data:any) { create_user(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/user/create", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/create", data);
} }
delete_user(id:number){ delete_user(id: number) {
var data={ var data = {
'uid':id 'uid': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/user/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/user/delete", data);
} }
check_firmware(devids:any) { check_firmware(devids: any) {
var data = { var data = {
'devids':devids 'devids': devids
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/check_firmware_update", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/check_firmware_update", data);
} }
get_firms(page:Number,size:Number,search:any) { get_firms(page: Number, size: Number, search: any) {
var data = { var data = {
'page':page, 'page': page,
'size':size, 'size': size,
'search':search 'search': search
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data);
} }
delete_firm(id:number){ delete_firm(id: number) {
var data={ var data = {
'id':id 'id': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/delete_from_repository", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/delete_from_repository", data);
} }
get_backups(data:any) { get_backups(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/backup/list", data); return this.MikroWizardRPC.sendJsonRequest("/api/backup/list", data);
} }
get_backup(id:number){ get_backup(id: number) {
var data = { var data = {
'id':id 'id': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data);
} }
restore_backup(id:number){ restore_backup(id: number) {
var data = { var data = {
'backupid':id 'backupid': id
} }
return this.MikroWizardRPC.sendJsonRequest("/api/backup/restore", data); return this.MikroWizardRPC.sendJsonRequest("/api/backup/restore", data);
} }
@ -500,63 +560,63 @@ export class dataProvider {
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_downloadable_firms", {}); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_downloadable_firms", {});
} }
download_firmware_to_repository(version:string){ download_firmware_to_repository(version: string) {
var data = { var data = {
'version':version 'version': version
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/download_firmware_to_repository", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/download_firmware_to_repository", data);
} }
save_firmware_setting(updatebehavior:string,firmwaretoinstall:string,firmwaretoinstallv6:string){ save_firmware_setting(updatebehavior: string, firmwaretoinstall: string, firmwaretoinstallv6: string) {
var data = { var data = {
'updatebehavior':updatebehavior, 'updatebehavior': updatebehavior,
'firmwaretoinstall':firmwaretoinstall, 'firmwaretoinstall': firmwaretoinstall,
'firmwaretoinstallv6':firmwaretoinstallv6 'firmwaretoinstallv6': firmwaretoinstallv6
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware_settings", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware_settings", data);
} }
update_firmware(devids:string){ update_firmware(devids: string) {
var data = { var data = {
'devids':devids 'devids': devids
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware", data);
} }
upgrade_firmware(devids:string){ upgrade_firmware(devids: string) {
var data = { var data = {
'devids':devids 'devids': devids
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/upgrade_firmware", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/upgrade_firmware", data);
} }
reboot_devices(devids:string){ reboot_devices(devids: string) {
var data = { var data = {
'devids':devids 'devids': devids
} }
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/reboot_devices", data); return this.MikroWizardRPC.sendJsonRequest("/api/firmware/reboot_devices", data);
} }
get_settings(){ get_settings() {
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/get_all", {}); return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/get_all", {});
} }
save_sys_setting(data:any){ save_sys_setting(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data); return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data);
} }
get_running_tasks(){ get_running_tasks() {
return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {}); return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {});
} }
stop_task(signal:number){ stop_task(signal: number) {
var data={ var data = {
'signal':signal 'signal': signal
} }
return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data); return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data);
} }
apply_update(action:string){ apply_update(action: string) {
var data={ var data = {
'action':action 'action': action
} }
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data); return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data);
} }
@ -566,62 +626,66 @@ export class dataProvider {
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {}); return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {});
} }
Add_cloner(data:any,members:any) { Add_cloner(data: any, members: any) {
data['members']=members; data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data); return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data);
} }
Delete_cloner(clonerid:number) { Delete_cloner(clonerid: number) {
var data={ var data = {
'clonerid':clonerid, 'clonerid': clonerid,
} }
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data); return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data);
} }
Edit_cloner(data:any,members:any) { Edit_cloner(data: any, members: any) {
data['members']=members; data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data); return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data);
} }
get_cloner_members(clonerid:number) { get_cloner_members(clonerid: number) {
var data={ var data = {
'clonerid':clonerid, 'clonerid': clonerid,
} }
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data); return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data);
} }
killSession(devid:number,item:any){ killSession(devid: number, item: any) {
var data={ var data = {
'devid':devid, 'devid': devid,
'item':item 'item': item
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data);
} }
getDhcpHistory(item:any){ getDhcpHistory(item: any) {
var data={ var data = {
'item':item 'item': item
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data); return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data);
} }
getNetworkMap(){ getNetworkMap() {
return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/get", {}); return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/get", {});
} }
bulk_add_devices(devices: any[]){ resetNetworkMap() {
return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/reset", {});
}
bulk_add_devices(devices: any[]) {
var data = { var data = {
'devices': devices 'devices': devices
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add", data);
} }
bulk_add_status(taskId: string){ bulk_add_status(taskId: string) {
var data = { var data = {
'taskId': taskId 'taskId': taskId
} }
return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add_status", data); return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add_status", data);
} }
group_firmware_action(groupId: number, action: string){ group_firmware_action(groupId: number, action: string) {
var data = { var data = {
'groupId': groupId, 'groupId': groupId,
'action': action 'action': action

View file

@ -1,6 +1,6 @@
import { Injectable, Inject } from '@angular/core'; import { Injectable, Inject } from '@angular/core';
import { HttpClient,HttpResponse } from '@angular/common/http'; import { HttpClient, HttpResponse } from '@angular/common/http';
class Cookies { // cookies doesn't work with Android default browser / Ionic class Cookies { // cookies doesn't work with Android default browser / Ionic
private session_id: string = ""; private session_id: string = "";

View file

@ -0,0 +1,167 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, interval, Subject, catchError, throwError, map, switchMap } from 'rxjs';
export interface VpnStats {
last_handshake: number;
rx_bytes: number;
tx_bytes: number;
rx_speed?: number;
tx_speed?: number;
nat_mode: string;
enabled: boolean;
allowed_ips: string;
}
export interface VpnPeer {
id: number;
public_key: string;
assigned_ip: string;
nat_mode: string;
split_targets: string[];
dns_server: string | null;
persistent_keepalive: number;
custom_interface: string | null;
linked_device_id: number | null;
is_enabled: boolean;
created_at: string;
stats?: VpnStats; // Joined from the /status API
is_managed?: boolean;
status?: 'online' | 'offline' | 'unreachable';
name?: string;
description?: string;
scan_status?: 'starting' | 'running' | 'completed' | 'failed' | null;
mt_user: string | null;
mt_pass: string | null;
mt_port: number | null;
}
export interface VpnServerConfig {
id?: number;
api_endpoint: string;
api_token: string | null;
vpn_subnet: string;
public_server_ip: string | null;
}
export interface VpnStatusResponse {
status: 'running' | 'setup_required' | 'error' | 'failed';
peers?: VpnPeer[];
server_config?: VpnServerConfig;
message?: string;
error?: string;
}
export interface VpnLiveStatusResponse {
server: {
rx_bytes: number;
tx_bytes: number;
rx_speed: number;
tx_speed: number;
};
peers: any[]; // The live endpoint returns an array of peer objects with full stats, same as /status
}
@Injectable({
providedIn: 'root'
})
export class VpnService {
private apiUrl = '/api/vpn';
constructor(private http: HttpClient) { }
// System Endpoints
getStatus(): Observable<VpnStatusResponse> {
return this.http.get<{ result: VpnStatusResponse }>(`${this.apiUrl}/status`).pipe(map(r => r.result));
}
getLiveStatus(): Observable<VpnLiveStatusResponse> {
return this.http.get<{ result: VpnLiveStatusResponse }>(`${this.apiUrl}/status/live`).pipe(
map(res => res.result),
catchError(err => throwError(() => err))
);
}
resetServerCounters(): Observable<any> {
return this.http.post(`${this.apiUrl}/server/reset-counters`, {}).pipe(
catchError(err => throwError(() => err))
);
}
getSystemConfig(): Observable<{ status: string, config: VpnServerConfig }> {
return this.http.get<{ result: { status: string, config: VpnServerConfig } }>(`${this.apiUrl}/system/config`).pipe(map(r => r.result));
}
updateSystemConfig(config: Partial<VpnServerConfig>): Observable<any> {
return this.http.post<{ result: any }>(`${this.apiUrl}/system/config`, config).pipe(map(r => r.result));
}
flushSystem(wipe_database: boolean): Observable<any> {
return this.http.post<{ result: any }>(`${this.apiUrl}/system/flush`, { wipe_database }).pipe(map(r => r.result));
}
// Peer Endpoints
addPeer(peerData: {
pubkey?: string,
custom_ip?: string,
nat_mode: 'full' | 'split' | 'off',
split_targets: string[],
persistent_keepalive: number,
custom_interface?: string,
name?: string,
description?: string,
mt_user?: string | null,
mt_pass?: string | null,
mt_port?: number | null
}): Observable<{ status: string, peer: VpnPeer }> {
return this.http.post<{ result: { status: string, peer: VpnPeer } }>(`${this.apiUrl}/peers/add`, peerData).pipe(map(r => r.result));
}
editPeer(peerData: {
pubkey: string,
custom_ip?: string,
nat_mode?: string,
split_targets?: string[],
persistent_keepalive?: number,
custom_interface?: string,
name?: string,
description?: string,
mt_user?: string | null,
mt_pass?: string | null,
mt_port?: number | null
}): Observable<any> {
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/edit`, peerData).pipe(map(r => r.result));
}
togglePeer(pubkey: string, enabled: boolean): Observable<any> {
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/toggle`, { pubkey, enabled }).pipe(map(r => r.result));
}
deletePeer(pubkey: string): Observable<any> {
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/delete`, { pubkey }).pipe(map(r => r.result));
}
getPeerConfig(pubkey: string): Observable<{ status: string, config: string }> {
return this.http.post<{ result: { status: string, config: string } }>(`${this.apiUrl}/peers/config`, { pubkey }).pipe(map(r => r.result));
}
getPeerMikrotikScript(pubkey: string): Observable<{ status: string, script: string }> {
return this.http.post<{ result: { status: string, script: string } }>(`${this.apiUrl}/peers/mikrotik-script`, { pubkey }).pipe(map(r => r.result));
}
scanLinkedDevice(pubkey: string): Observable<any> {
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/scan`, { pubkey }).pipe(map(r => r.result));
}
getPeerQrCode(pubkey: string): Observable<Blob> {
return this.http.post(`${this.apiUrl}/peers/qrcode`, { pubkey }, { responseType: 'blob' });
}
resetPeerCounters(pubkey: string): Observable<any> {
// pubkey must be url-encoded to safely pass base64 across URL path
return this.http.post(`${this.apiUrl}/peer/${encodeURIComponent(pubkey)}/reset-counters`, {}).pipe(
catchError(err => throwError(() => err))
);
}
}

View file

@ -0,0 +1,16 @@
<div class="license-overlay" *ngIf="isExpired$ | async">
<div class="overlay-content">
<div class="icon-container">
<i class="fa-solid fa-triangle-exclamation"></i>
</div>
<h2>Pro Feature: License Expired</h2>
<p>This view is restricted to pro-licensed customers. Your license has either expired or is not valid for this
feature.</p>
<div class="actions">
<a href="https://mikrowizard.com/register-new-mikrowizard/" target="_blank" class="btn-renew">
<i class="fa-solid fa-cart-shopping"></i> Renew License
</a>
<button class="btn-dismiss" (click)="dismiss()">Later</button>
</div>
</div>
</div>

View file

@ -0,0 +1,139 @@
.license-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
border-radius: inherit;
animation: fadeIn 0.4s ease-out;
.overlay-content {
background: rgba(255, 255, 255, 0.95);
padding: 2.5rem;
border-radius: 24px;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
max-width: 480px;
width: 90%;
display: flex;
flex-direction: column;
align-items: center;
transform: translateY(0);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
transform: translateY(-5px);
}
.icon-container {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #fde8e8 0%, #f9d2d2 100%);
color: #e55353;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
margin-bottom: 1.5rem;
position: relative;
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 2px solid #e55353;
animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite;
opacity: 0;
}
}
h2 {
color: #1a1c23;
font-weight: 800;
margin-bottom: 0.75rem;
font-size: 1.75rem;
letter-spacing: -0.025em;
}
p {
color: #4b5563;
line-height: 1.7;
margin-bottom: 2.5rem;
font-size: 1.05rem;
}
.actions {
display: flex;
gap: 1.25rem;
width: 100%;
.btn-renew {
flex: 3;
background: linear-gradient(135deg, #e55353 0%, #c53030 100%);
color: white;
padding: 0.875rem 1.5rem;
border-radius: 14px;
text-decoration: none;
font-weight: 700;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
border: none;
cursor: pointer;
font-size: 1rem;
box-shadow: 0 4px 12px rgba(229, 83, 83, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(229, 83, 83, 0.5);
filter: brightness(1.1);
color: white;
}
&:active {
transform: translateY(0);
}
}
.btn-dismiss {
flex: 1;
background: #f3f4f6;
color: #6b7280;
padding: 0.875rem 1rem;
border-radius: 14px;
font-weight: 600;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
font-size: 0.95rem;
&:hover {
background: #e5e7eb;
color: #374151;
}
}
}
}
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes ping {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(2); opacity: 0; }
}

View file

@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { LicenseService } from '../../../providers/license.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-license-expired-overlay',
templateUrl: './license-expired-overlay.component.html',
styleUrls: ['./license-expired-overlay.component.scss']
})
export class LicenseExpiredOverlayComponent implements OnInit {
public isExpired$: Observable<boolean>;
constructor(private licenseService: LicenseService) {
this.isExpired$ = this.licenseService.isExpired$;
}
ngOnInit(): void {
}
dismiss(): void {
// Locally hide the overlay for the current component session if needed
// But usually, it should stay until the license is fixed.
}
}

View file

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LicenseExpiredOverlayComponent } from './components/license-expired-overlay/license-expired-overlay.component';
@NgModule({
declarations: [
LicenseExpiredOverlayComponent
],
imports: [
CommonModule
],
exports: [
LicenseExpiredOverlayComponent
]
})
export class SharedModule { }

View file

@ -1,10 +1,10 @@
<c-row> <c-row>
<c-col xs> <c-col xs>
<c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}"> <c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}">
<c-card-header> <c-card-header>
<c-row> <c-row>
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;"> <c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
<h5>Accunting Logs <h5>Accounting Logs
<a style="cursor: pointer;" (click)="reinitgrid('none','none')"><i <a style="cursor: pointer;" (click)="reinitgrid('none','none')"><i
*ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate" *ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate"
style="color: #74C0FC;"></i> style="color: #74C0FC;"></i>
@ -14,8 +14,10 @@
</h5> </h5>
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID <c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
{{devid}}</c-badge> {{devid}}</c-badge>
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']"> <c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time. *ngIf="!filters['start_time'] && !filters['end_time']">
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
default. Use filters to modify the date and time.
</c-alert> </c-alert>
</c-col> </c-col>
<c-col xs [lg]="1"> <c-col xs [lg]="1">
@ -73,49 +75,143 @@
</c-col> </c-col>
</div> </div>
</c-row> </c-row>
<gui-grid [rowDetail]="rowDetail" [source]="source" [columnMenu]="columnMenu" [paging]="paging" <div class="mb-3 d-flex justify-content-end">
[sorting]="sorting" [infoPanel]="infoPanel" [autoResizeWidth]=true> <span class="p-input-icon-left table-search-input">
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER"> <i class="pi pi-search"></i>
<ng-template let-value="item.index" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search commands/users..."
{{value}} (input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="Device Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
{{value}} [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
</ng-template> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</gui-grid-column> [globalFilterFields]="['username', 'name', 'address', 'config', 'section', 'action']" selectionMode="single"
<gui-grid-column header="Device IP" field="devip"> (onRowSelect)="showLogDetails($event.data)">
<ng-template let-value="item.devip" let-item="item" let-index="index">
{{value}} <ng-template pTemplate="header">
</ng-template> <tr>
</gui-grid-column> <th pSortableColumn="index" style="width: 5rem" pResizableColumn>
<gui-grid-column header="Username" field="username"> <div class="justify-between">
<ng-template let-value="item.username" let-item="item" let-index="index"> <span>#No</span>
{{value}} <p-sortIcon field="index"></p-sortIcon>
</ng-template> </div>
</gui-grid-column> </th>
<gui-grid-column header="Action" field="action"> <th pSortableColumn="name" pResizableColumn>
<ng-template let-value="item.action" let-item="item" let-index="index"> <div class="justify-between">
<div>{{value}}</div> <span>Device Name</span>
</ng-template> <span>
</gui-grid-column> <p-sortIcon field="name"></p-sortIcon>
<gui-grid-column header="Section" field="section"> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
<ng-template let-value="item.section" let-item="item" let-index="index"> </span>
{{value}} </div>
</ng-template> </th>
</gui-grid-column> <th pSortableColumn="devip" pResizableColumn>
<gui-grid-column header="Date" field="created"> <div class="justify-between">
<ng-template let-value="item.created" let-item="item" let-index="index"> <span>Device IP</span>
{{value}} <span>
</ng-template> <p-sortIcon field="devip"></p-sortIcon>
</gui-grid-column> <p-columnFilter type="text" field="devip" display="menu" class="ms-auto" />
<gui-grid-column header="Message" field="message" [enabled]="false"> </span>
<ng-template let-value="item.message" let-item="item" let-index="index"> </div>
{{value}} </th>
</ng-template> <th pSortableColumn="username" pResizableColumn>
</gui-grid-column> <div class="justify-between">
</gui-grid> <span>User Name</span>
<span>
<p-sortIcon field="username"></p-sortIcon>
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="action" pResizableColumn>
<div class="justify-between">
<span>Action</span>
<span>
<p-sortIcon field="action"></p-sortIcon>
<p-columnFilter type="text" field="action" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="section" pResizableColumn>
<div class="justify-between">
<span>Section</span>
<span>
<p-sortIcon field="section"></p-sortIcon>
<p-columnFilter type="text" field="section" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="created" pResizableColumn>
<div class="justify-between">
<span>Date</span>
<span>
<p-sortIcon field="created"></p-sortIcon>
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
</span>
</div>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr [pSelectableRow]="item" style="cursor: pointer;">
<td class="text-center">{{item.index}}</td>
<td class="fw-bold">{{item.name}}</td>
<td>{{item.devip}}</td>
<td>{{item.username}}</td>
<td>{{item.action}}</td>
<td>{{item.section}}</td>
<td class="small">{{item.created}}</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="7" class="text-center p-4">No accounting logs found.</td>
</tr>
</ng-template>
</p-table>
<!-- Details Drawer -->
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '500px'}"
header="Accounting Log Details">
<div *ngIf="selectedLog" class="p-3">
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-desktop me-2 text-primary"></i>Device Info</h5>
<div class="mb-3">
<div class="h4 m-0 text-primary">{{selectedLog.name}}</div>
<div class="text-muted">{{selectedLog.devip}}</div>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-user me-2 text-primary"></i>Session Details</h5>
<div class="row mb-2">
<div class="col-5 fw-bold">User Name:</div>
<div class="col-7">{{selectedLog.username}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold">User IP:</div>
<div class="col-7">{{selectedLog.address}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold">Section:</div>
<div class="col-7">{{selectedLog.section}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold">Action:</div>
<div class="col-7">{{selectedLog.action}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold">Exec time:</div>
<div class="col-7 small">{{selectedLog.created}}</div>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-code me-2 text-primary"></i>Executed Config</h5>
<div class="bg-dark text-success p-3 rounded shadow-inner"
style="max-height: 400px; overflow-y: auto; font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace; font-size: 0.9rem; border: 1px solid #333;">
<pre class="m-0" style="white-space: pre-wrap;">{{selectedLog.config}}</pre>
</div>
</div>
</p-drawer>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>

View file

@ -1,19 +1,8 @@
import { Component, OnInit, ViewEncapsulation,Input } from "@angular/core"; import { Component, OnInit, ViewChild, ViewEncapsulation, Input } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiRowDetail,
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
@ -26,10 +15,14 @@ import { formatInTimeZone } from "date-fns-tz";
}) })
export class AccComponent implements OnInit { export class AccComponent implements OnInit {
@Input() component_devid: any=false; @Input() component_devid: any=false;
public uid: number; public uid!: number;
public uname: string; public uname!: string;
public tz: string; public tz!: string;
public filterText: string; public filterText!: string;
public detailsVisible: boolean = false;
public selectedLog: any = null;
@ViewChild('dt') table!: Table;
public reloading: boolean = false; public reloading: boolean = false;
public filters: any = { public filters: any = {
devid: false, devid: false,
@ -75,78 +68,20 @@ export class AccComponent implements OnInit {
} }
} }
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public Selectedrows: any; public selected_rows: any[] = [];
public Selectedrows: any[] = [];
public devid: number = 0; public devid: number = 0;
public sorting = {
enabled: true,
multiSorting: true,
};
rowDetail: GuiRowDetail = {
enabled: true,
template: (item) => {
return `
<div class='log-detail' style="width: 355px;">
<h1>${item.name}</h1>
<small>${item.devip}</small>
<table>
<tr>
<td>User Address</td>
<td>${item.address}</td>
</tr>
<tr>
<td>User Name</td>
<td>${item.username}</td>
</tr>
<tr>
<td>Connection Type</td>
<td>${item.ctype}</td>
</tr>
<tr>
<td>Section</td>
<td>${item.section}</td>
</tr>
<tr>
<td>Exec time</td>
<td>${item.created}</td>
</tr>
</table>
<div class="code-title">Executed Config</div>
<code>
${item.config}
</code>
</div>`;
},
};
public paging: GuiPaging = { showLogDetails(log: any) {
enabled: true, this.selectedLog = log;
page: 1, this.detailsVisible = true;
pageSize: 10, }
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
sort: true, }
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
reinitgrid(field: string, $event: any) { reinitgrid(field: string, $event: any) {
if (field == "start") this.filters["start_time"] = $event.target.value; if (field == "start") this.filters["start_time"] = $event.target.value;
@ -170,9 +105,10 @@ export class AccComponent implements OnInit {
this.initGridTable(); this.initGridTable();
} }
OnDestroy(): void {} OnDestroy(): void {}
onSelectedRows(rows: Array<GuiSelectedRow>): void { onSelectionChange(value: any[]) {
this.rows = rows; this.selected_rows = value;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id); this.Selectedrows = value.map(item => item.id);
this.rows = value;
} }
removefilter(filter: any) { removefilter(filter: any) {

View file

@ -13,7 +13,9 @@ import {
import { AccRoutingModule } from "./acc-routing.module"; import { AccRoutingModule } from "./acc-routing.module";
import { AccComponent } from "./acc.component"; import { AccComponent } from "./acc.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule } from 'primeng/table';
import { DrawerModule } from 'primeng/drawer';
import { InputTextModule } from 'primeng/inputtext';
import { MatDatepickerModule } from "@angular/material/datepicker"; import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatInputModule } from "@angular/material/input"; import { MatInputModule } from "@angular/material/input";
@ -32,7 +34,9 @@ import { FormsModule } from "@angular/forms";
ButtonModule, ButtonModule,
FormModule, FormModule,
ButtonModule, ButtonModule,
GuiGridModule, TableModule,
DrawerModule,
InputTextModule,
CollapseModule, CollapseModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,

View file

@ -1,6 +1,6 @@
<c-row> <c-row>
<c-col xs> <c-col xs>
<c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}"> <c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}">
<c-card-header> <c-card-header>
<c-row> <c-row>
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;"> <c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
@ -14,8 +14,10 @@
</h5> </h5>
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID <c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
{{devid}}</c-badge> {{devid}}</c-badge>
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']"> <c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time. *ngIf="!filters['start_time'] && !filters['end_time']">
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
default. Use filters to modify the date and time.
</c-alert> </c-alert>
</c-col> </c-col>
<c-col xs [lg]="1"> <c-col xs [lg]="1">
@ -95,64 +97,180 @@
</div> </div>
</c-row> </c-row>
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting" <div class="mb-3 d-flex justify-content-end gap-2">
[infoPanel]="infoPanel" [autoResizeWidth]=true> <span class="p-input-icon-left table-search-input">
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER"> <i class="pi pi-search"></i>
<ng-template let-value="item.index" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
{{value}} class="form-control-sm" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="Device Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<i *ngIf="item.stype=='local'" cTooltip="local user"
style="color: rgb(255, 42, 0); margin-right: 3px;font-size: .7em;" class="fa-solid fa-user-tie"></i>
<i *ngIf="item.stype=='radius'" cTooltip="Update failed"
style="color: rgb(9, 97, 20); margin-right: 3px;font-size: .7em;" class="fa-solid fa-server"></i>
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Device IP" field="devip">
<ng-template let-value="item.devip" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Username" field="username">
<ng-template let-value="item.username" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="With" field="by">
<ng-template let-value="item.by" let-item="item" let-index="index">
<div>{{value}}</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Time/Msg" field="duration"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<ng-template let-value="item.duration" let-item="item" let-index="index"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
<span *ngIf="item.ltype!='failed'">{{value}}</span> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
<span *ngIf="item.ltype=='failed'">{{item.message}}</span> [globalFilterFields]="['username', 'name', 'ip', 'devip', 'by']" [(selection)]="selected_rows"
(onRowSelect)="showLogDetails($event.data)" selectionMode="single">
</ng-template> <ng-template pTemplate="header">
</gui-grid-column> <tr>
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
<div class="justify-between">
<span>#No</span>
<p-sortIcon field="index"></p-sortIcon>
</div>
</th>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Device Name</span>
<span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="devip" pResizableColumn>
<div class="justify-between">
<span>Device IP</span>
<span>
<p-sortIcon field="devip"></p-sortIcon>
<p-columnFilter type="text" field="devip" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="username" pResizableColumn>
<div class="justify-between">
<span>Username</span>
<span>
<p-sortIcon field="username"></p-sortIcon>
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="by" pResizableColumn>
<div class="justify-between">
<span>With</span>
<span>
<p-sortIcon field="by"></p-sortIcon>
<p-columnFilter type="text" field="by" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="ip" pResizableColumn>
<div class="justify-between">
<span>IP Address</span>
<span>
<p-sortIcon field="ip"></p-sortIcon>
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="duration" pResizableColumn>
<div class="justify-between">
<span>Time/Msg</span>
<span>
<p-sortIcon field="duration"></p-sortIcon>
<p-columnFilter type="text" field="duration" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="ltype" style="width: 120px" pResizableColumn>
<div class="justify-between">
<span>State</span>
<p-sortIcon field="ltype"></p-sortIcon>
<p-columnFilter type="text" field="ltype" display="menu" class="ms-auto" />
</div>
</th>
<th pSortableColumn="created" pResizableColumn>
<div class="justify-between">
<span>Date</span>
<p-sortIcon field="created"></p-sortIcon>
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
</div>
</th>
</tr>
</ng-template>
<gui-grid-column header="State" field="ltype" [width]="110"> <ng-template pTemplate="body" let-item>
<ng-template let-value="item.ltype" let-item="item.id" let-index="index"> <tr [pSelectableRow]="item" style="cursor: pointer;">
<c-badge color="success" *ngIf="value=='loggedin'"> Logged In</c-badge> <td class="text-center">{{item.index}}</td>
<c-badge color="warning" *ngIf="value=='loggedout'"> Logged Out</c-badge> <td>
<c-badge color="danger" *ngIf="value=='failed'"> Failed</c-badge> <i *ngIf="item.stype=='local'" pTooltip="local user" style="color: #ff2a00; margin-right: 3px;"
</ng-template> class="fa-solid fa-user-tie"></i>
</gui-grid-column> <i *ngIf="item.stype=='radius'" pTooltip="radius user" style="color: #096114; margin-right: 3px;"
<gui-grid-column header="Date" field="created"> class="fa-solid fa-server"></i>
<ng-template let-value="item.created" let-item="item" let-index="index"> {{item.name}}
{{value}} </td>
</ng-template> <td>{{item.devip}}</td>
</gui-grid-column> <td>{{item.username}}</td>
</gui-grid> <td>{{item.by}}</td>
<td>{{item.ip}}</td>
<td>
<span *ngIf="item.ltype!='failed'">{{item.duration}}</span>
<span *ngIf="item.ltype=='failed'" class="text-muted small">{{item.message}}</span>
</td>
<td class="text-center">
<c-badge color="success" *ngIf="item.ltype=='loggedin'"> Logged In</c-badge>
<c-badge color="warning" *ngIf="item.ltype=='loggedout'"> Logged Out</c-badge>
<c-badge color="danger" *ngIf="item.ltype=='failed'"> Failed</c-badge>
</td>
<td class="small">{{item.created}}</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="9" class="text-center p-4">No authentication logs found.</td>
</tr>
</ng-template>
</p-table>
<!-- Details Drawer -->
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '450px'}" header="Auth Log Details">
<div *ngIf="selectedLog" class="p-3">
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-shield me-2 text-primary"></i>Session Info</h5>
<div class="row mb-2">
<div class="col-4 fw-bold">Device:</div>
<div class="col-8">{{selectedLog.name}} ({{selectedLog.devip}})</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">User:</div>
<div class="col-8">{{selectedLog.username}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Method:</div>
<div class="col-8">{{selectedLog.by}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Status:</div>
<div class="col-8">
<c-badge color="success" *ngIf="selectedLog.ltype=='loggedin'"> Logged In</c-badge>
<c-badge color="warning" *ngIf="selectedLog.ltype=='loggedout'"> Logged Out</c-badge>
<c-badge color="danger" *ngIf="selectedLog.ltype=='failed'"> Failed</c-badge>
</div>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-map-marker me-2 text-primary"></i>Connection</h5>
<div class="row mb-2">
<div class="col-4 fw-bold">IP/MAC:</div>
<div class="col-8">{{selectedLog.ip}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Duration:</div>
<div class="col-8">{{selectedLog.duration}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Date:</div>
<div class="col-8 small">{{selectedLog.created}}</div>
</div>
<div *ngIf="selectedLog.message" class="mt-4">
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-envelope me-2 text-primary"></i>System Message</h5>
<div class="bg-light p-2 rounded small border text-break">
{{selectedLog.message}}
</div>
</div>
</div>
</p-drawer>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>

View file

@ -1,18 +1,8 @@
import { Component, OnInit, ViewEncapsulation,Input } from "@angular/core"; import { Component, OnInit, ViewChild, ViewEncapsulation, Input } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
interface IUser { interface IUser {
@ -37,10 +27,14 @@ interface IUser {
}) })
export class AuthComponent implements OnInit { export class AuthComponent implements OnInit {
@Input() component_devid: any=false; @Input() component_devid: any=false;
public uid: number; public uid!: number;
public uname: string; public uname!: string;
public tz: string = "UTC"; public tz: string = "UTC";
public filterText: string; public filterText!: string;
public detailsVisible: boolean = false;
public selectedLog: any = null;
@ViewChild('dt') table!: Table;
public devid: number = 0; public devid: number = 0;
public reloading: boolean = false; public reloading: boolean = false;
public filters: any = { public filters: any = {
@ -87,42 +81,19 @@ export class AuthComponent implements OnInit {
} }
} }
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public Selectedrows: any; public selected_rows: any[] = []; // Used by p-table selection
public Selectedrows: any[] = []; // ID array for legacy actions
public sorting = { showLogDetails(log: any) {
enabled: true, this.selectedLog = log;
multiSorting: true, this.detailsVisible = true;
}; }
public paging: GuiPaging = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
page: 1, }
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
reinitgrid(field: string, $event: any) { reinitgrid(field: string, $event: any) {
if (field == "start") this.filters["start_time"] = $event.target.value; if (field == "start") this.filters["start_time"] = $event.target.value;
@ -179,9 +150,10 @@ export class AuthComponent implements OnInit {
} }
this.initGridTable(); this.initGridTable();
} }
onSelectedRows(rows: Array<GuiSelectedRow>): void { onSelectionChange(value: any[]) {
this.rows = rows; this.selected_rows = value;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id); this.Selectedrows = value.map(item => item.id);
this.rows = value;
} }
removefilter(filter: any) { removefilter(filter: any) {

View file

@ -11,7 +11,10 @@ import {
} from '@coreui/angular'; } from '@coreui/angular';
import { AuthRoutingModule } from './auth-routing.module'; import { AuthRoutingModule } from './auth-routing.module';
import { AuthComponent } from './auth.component'; import { AuthComponent } from './auth.component';
import { GuiGridModule } from '@generic-ui/ngx-grid'; import { TableModule } from 'primeng/table';
import { DrawerModule } from 'primeng/drawer';
import { InputTextModule } from 'primeng/inputtext';
import { TooltipModule } from 'primeng/tooltip';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@ -27,7 +30,10 @@ import {MatSelectModule} from '@angular/material/select';
GridModule, GridModule,
FormsModule, FormsModule,
ButtonModule, ButtonModule,
GuiGridModule, TableModule,
DrawerModule,
InputTextModule,
TooltipModule,
CollapseModule, CollapseModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,

View file

@ -27,8 +27,7 @@
title="Remove from comparison"></i> title="Remove from comparison"></i>
</c-badge> </c-badge>
</div> </div>
<button *ngIf="compareitems.length > 1" (click)="start_compare()" <button *ngIf="compareitems.length > 1" (click)="start_compare()" cButton color="success" size="sm">
cButton color="success" size="sm">
<i class="fa-solid fa-code-compare me-1"></i>Compare <i class="fa-solid fa-code-compare me-1"></i>Compare
</button> </button>
<button (click)="clearAllCompare()" cButton color="secondary" size="sm" variant="outline"> <button (click)="clearAllCompare()" cButton color="secondary" size="sm" variant="outline">
@ -84,8 +83,8 @@
<i class="fa-solid fa-magnifying-glass me-1 text-primary"></i>Config Search <i class="fa-solid fa-magnifying-glass me-1 text-primary"></i>Config Search
</label> </label>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']" <input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']" matInput
matInput placeholder="Search in configurations..."> placeholder="Search in configurations...">
</mat-form-field> </mat-form-field>
</div> </div>
</c-col> </c-col>
@ -93,65 +92,104 @@
</c-card-body> </c-card-body>
</c-card> </c-card>
</div> </div>
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting" <div class="mb-3 d-flex justify-content-end">
[infoPanel]="infoPanel" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left table-search-input">
[autoResizeWidth]=true> <i class="pi pi-search"></i>
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER"> <input type="text" pInputText placeholder="Search backups..."
<ng-template let-value="item.index" let-item="item" let-index="index"> (input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
{{value}} </span>
</ng-template> </div>
</gui-grid-column>
<gui-grid-column header="Device Name" field="devname"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<ng-template let-value="item.devname" let-item="item" let-index="index"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
{{value}} [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</ng-template> [globalFilterFields]="['devname', 'devip', 'createdC', 'filesize']" [loading]="loading">
</gui-grid-column>
<gui-grid-column header="Device IP" field="devip"> <ng-template pTemplate="header">
<ng-template let-value="item.devip" let-item="item" let-index="index"> <tr>
{{value}} <th pSortableColumn="index" style="width: 70px" class="text-center" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>#No</span>
<gui-grid-column header="backup Time" field="createdC"> <p-sortIcon field="index"></p-sortIcon>
<ng-template let-value="item.createdC" let-item="item" let-index="index"> </div>
{{value}} </th>
</ng-template> <th pSortableColumn="devname" pResizableColumn>
</gui-grid-column> <div class="justify-between">
<gui-grid-column header="File Size" field="filesize"> <span>Device Name</span>
<ng-template let-value="item.filesize" let-item="item" let-index="index"> <span>
{{value}} <p-sortIcon field="devname"></p-sortIcon>
</ng-template> <p-columnFilter type="text" field="devname" display="menu" class="ms-auto" />
</gui-grid-column> </span>
<gui-grid-column header="MAC" field="devmac" [enabled]="false"> </div>
<ng-template let-value="item.devmac" let-item="item" let-index="index"> </th>
{{value}} <th pSortableColumn="devip" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>Device IP</span>
<gui-grid-column header="Actions" field="id" width="280"> <span>
<ng-template let-value="item.id" let-item="item" let-index="index"> <p-sortIcon field="devip"></p-sortIcon>
<div class="d-flex gap-1"> <p-columnFilter type="text" field="devip" display="menu" class="ms-auto" />
<button cButton [disabled]="backuploading" color="primary" size="sm" </span>
(click)="ShowBackup(item)" variant="outline" title="View backup content"> </div>
<i *ngIf="backuploading && currentBackup?.id === item.id" </th>
class="fa-solid fa-spinner fa-spin me-1"></i> <th pSortableColumn="createdC" pResizableColumn>
<i *ngIf="!backuploading || currentBackup?.id !== item.id" <div class="justify-between">
class="fa-solid fa-eye me-1"></i> <span>Backup Time</span>
View <span>
</button> <p-sortIcon field="createdC"></p-sortIcon>
<button *ngIf="ispro" cButton color="info" size="sm" variant="outline" <p-columnFilter type="text" field="createdC" display="menu" class="ms-auto" />
(click)="add_for_compare(item)" title="Add to comparison" </span>
[disabled]="isInCompareList(item)"> </div>
<i class="fa-solid fa-code-compare me-1"></i> </th>
{{isInCompareList(item) ? 'Added' : 'Compare'}} <th pSortableColumn="filesize" pResizableColumn>
</button> <div class="justify-between">
<button *ngIf="ispro" cButton color="warning" size="sm" variant="outline" <span>File Size</span>
(click)="restore_backup(false, false, item)" title="Restore this backup"> <span>
<i class="fa-solid fa-rotate-left me-1"></i> <p-sortIcon field="filesize"></p-sortIcon>
Restore <p-columnFilter type="text" field="filesize" display="menu" class="ms-auto" />
</button> </span>
</div> </div>
</ng-template> </th>
</gui-grid-column> <th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
</gui-grid> </tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td class="text-center">{{item.index}}</td>
<td>{{item.devname}}</td>
<td>{{item.devip}}</td>
<td>{{item.createdC}}</td>
<td>{{item.filesize}}</td>
<td class="text-center">
<div class="d-flex gap-1 justify-content-center">
<button cButton [disabled]="backuploading" color="primary" size="sm" (click)="ShowBackup(item)"
variant="outline" title="View backup content">
<i *ngIf="backuploading && currentBackup?.id === item.id"
class="fa-solid fa-spinner fa-spin me-1"></i>
<i *ngIf="!backuploading || currentBackup?.id !== item.id" class="fa-solid fa-eye me-1"></i>
View
</button>
<button *ngIf="ispro" cButton color="info" size="sm" variant="outline" (click)="add_for_compare(item)"
title="Add to comparison" [disabled]="isInCompareList(item)">
<i class="fa-solid fa-code-compare me-1"></i>
{{isInCompareList(item) ? 'Added' : 'Compare'}}
</button>
<button *ngIf="ispro" cButton color="warning" size="sm" variant="outline"
(click)="restore_backup(false, false, item)" title="Restore this backup">
<i class="fa-solid fa-rotate-left me-1"></i>
Restore
</button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="6" class="text-center p-4">No backups found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>
@ -166,11 +204,12 @@
<!-- <pre style="height: 100%;"> <!-- <pre style="height: 100%;">
<code *ngIf="!loading" language="yaml" style="height:70vh" [highlight]="codeForHighlightAuto" <code *ngIf="!loading" language="yaml" style="height:70vh" [highlight]="codeForHighlightAuto"
lineNumbers></code></pre> --> lineNumbers></code></pre> -->
<div highlight-js [lang]="hlang" [options]="{}" >{{codeForHighlightAuto}}</div> <div highlight-js [lang]="hlang" [options]="{}">{{codeForHighlightAuto}}</div>
</c-modal-body> </c-modal-body>
<c-modal-footer style="justify-content: space-between;"> <c-modal-footer style="justify-content: space-between;">
<button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null" (click)="copy_this()" cButton color="secondary"> <button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null"
(click)="copy_this()" cButton color="secondary">
<i class="fa-regular fa-copy"></i> To clipboard <i class="fa-regular fa-copy"></i> To clipboard
</button> </button>
<div> <div>
@ -290,8 +329,7 @@
<button (click)="cancelCriticalRestore()" cButton color="secondary"> <button (click)="cancelCriticalRestore()" cButton color="secondary">
<i class="fa-solid fa-times me-1"></i>Cancel <i class="fa-solid fa-times me-1"></i>Cancel
</button> </button>
<button (click)="restore_backup(true, true)" cButton color="danger" <button (click)="restore_backup(true, true)" cButton color="danger" [disabled]="confirmationText !== 'CONFIRM'">
[disabled]="confirmationText !== 'CONFIRM'">
<i class="fa-solid fa-rotate-left me-1"></i>RESTORE BACKUP <i class="fa-solid fa-rotate-left me-1"></i>RESTORE BACKUP
</button> </button>
</c-modal-footer> </c-modal-footer>

View file

@ -1,18 +1,8 @@
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core"; import { Component, OnInit, QueryList, ViewChildren, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { ToasterComponent } from "@coreui/angular"; import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component"; import { AppToastComponent } from "../toast-simple/toast.component";
@ -22,10 +12,10 @@ import { AppToastComponent } from "../toast-simple/toast.component";
styleUrls: ["backups.component.scss"], styleUrls: ["backups.component.scss"],
}) })
export class BackupsComponent implements OnInit { export class BackupsComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public tz: string = "UTC"; public tz: string = "UTC";
public filterText: string; public filterText: string = '';
public filters: any = {}; public filters: any = {};
public codeForHighlightAuto: string = ""; public codeForHighlightAuto: string = "";
public ispro: boolean = false; public ispro: boolean = false;
@ -69,34 +59,19 @@ export class BackupsComponent implements OnInit {
return value !== undefined && value !== null && value !== ""; return value !== undefined && value !== null && value !== "";
} }
} }
@ViewChild("dt") table!: Table;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public backuploading : boolean=false; public backuploading: boolean = false;
public rows: any = []; public rows: any = [];
public Selectedrows: any; public Selectedrows: any;
public BakcupModalVisible: boolean = false; public BakcupModalVisible: boolean = false;
public devid: number = 0; public devid: number = 0;
public filters_visible: boolean = false; public filters_visible: boolean = false;
public currentBackup:any=false; public currentBackup: any = false;
public hlang:string=''; public hlang: string = '';
public sorting = {
enabled: true,
multiSorting: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
toasterForm = { toasterForm = {
autohide: true, autohide: true,
@ -106,24 +81,9 @@ export class BackupsComponent implements OnInit {
closeButton: true, closeButton: true,
}; };
public columnMenu: GuiColumnMenu = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
sort: true, }
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
ngOnInit(): void { ngOnInit(): void {
this.devid = Number(this.route.snapshot.paramMap.get("devid")); this.devid = Number(this.route.snapshot.paramMap.get("devid"));

View file

@ -17,7 +17,9 @@ import {
import { BackupsRoutingModule } from "./backups-routing.module"; import { BackupsRoutingModule } from "./backups-routing.module";
import { BackupsComponent } from "./backups.component"; import { BackupsComponent } from "./backups.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule } from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { TooltipModule } from 'primeng/tooltip';
import { MatDatepickerModule } from "@angular/material/datepicker"; import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatInputModule } from "@angular/material/input"; import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field"; import { MatFormFieldModule } from "@angular/material/form-field";
@ -35,7 +37,9 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
FormModule, FormModule,
FormsModule, FormsModule,
ButtonModule, ButtonModule,
GuiGridModule, TableModule,
InputTextModule,
TooltipModule,
CollapseModule, CollapseModule,
BadgeModule, BadgeModule,
AlertModule, AlertModule,

View file

@ -1,3 +1,4 @@
<div style="position: relative;">
<c-row> <c-row>
<c-col xs> <c-col xs>
<c-card class="mb-4"> <c-card class="mb-4">
@ -13,107 +14,172 @@
</c-row> </c-row>
</c-card-header> </c-card-header>
<c-card-body> <c-card-body>
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting" <div class="mb-3 d-flex justify-content-end">
[infoPanel]="infoPanel" [autoResizeWidth]=true> <span class="p-input-icon-left table-search-input">
<gui-grid-column header="Name" field="name"> <i class="pi pi-search"></i>
<ng-template let-value="item.name" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search cloner..." (input)="applyFilterGlobal($event, 'contains')"
<strong>{{value}}</strong> class="form-control-sm" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="Description" field="description">
<ng-template let-value="item.description" let-item="item" let-index="index"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
{{value}} [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
</ng-template> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</gui-grid-column> [globalFilterFields]="['name', 'description', 'direction']" [loading]="loading">
<gui-grid-column header="Direction" field="direction">
<ng-template let-value="item.direction" let-item="item" let-index="index"> <ng-template pTemplate="header">
<c-badge [color]="value == 'twoway' ? 'success' : 'warning'">{{value == 'twoway' ? 'Two Way' : 'Master Mode'}}</c-badge> <tr>
</ng-template> <th pSortableColumn="name" pResizableColumn>
</gui-grid-column> <div class="justify-between">
<gui-grid-column header="Live Mode" field="live_mode"> <span>Name</span>
<ng-template let-value="item.live_mode" let-item="item" let-index="index"> <span>
<c-badge [color]="value ? 'success' : 'secondary'">{{value ? 'Active' : 'Inactive'}}</c-badge> <p-sortIcon field="name"></p-sortIcon>
</ng-template> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</gui-grid-column> </span>
<gui-grid-column header="Schedule" field="desc_cron"> </div>
<ng-template let-value="item.desc_cron" let-item="item" let-index="index"> </th>
{{value}} <th pSortableColumn="description" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>Description</span>
<gui-grid-column header="Actions" width="120" field="action"> <span>
<ng-template let-value="item.id" let-item="item" let-index="index"> <p-sortIcon field="description"></p-sortIcon>
<button cButton color="warning" size="sm" (click)="editAddCloner(item,'edit');"><i <p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
class="fa-regular fa-pen-to-square"></i></button> </span>
<!-- <button cButton color="info" size="sm" (click)="confirm_run(item);" class="mx-1"><i </div>
class="fa-solid fa-bolt"></i></button> --> </th>
<button class=" mx-1" cButton color="danger" size="sm" (click)="confirm_delete(item);"><i <th pSortableColumn="direction" pResizableColumn>
class="fa-regular fa-trash-can"></i></button> <div class="justify-between">
</ng-template> <span>Direction</span>
</gui-grid-column> <span>
</gui-grid> <p-sortIcon field="direction"></p-sortIcon>
<p-columnFilter type="text" field="direction" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="live_mode" class="text-center" pResizableColumn>
<div class="justify-between">
<span>Live Mode</span>
<span>
<p-sortIcon field="live_mode"></p-sortIcon>
<p-columnFilter type="boolean" field="live_mode" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="desc_cron" pResizableColumn>
<div class="justify-between">
<span>Schedule</span>
<span>
<p-sortIcon field="desc_cron"></p-sortIcon>
<p-columnFilter type="text" field="desc_cron" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><strong>{{item.name}}</strong></td>
<td>{{item.description}}</td>
<td>
<c-badge [color]="item.direction == 'twoway' ? 'success' : 'warning'">
{{item.direction == 'twoway' ? 'Two Way' : 'Master Mode'}}
</c-badge>
</td>
<td class="text-center">
<c-badge [color]="item.live_mode ? 'success' : 'secondary'">{{item.live_mode ? 'Active' :
'Inactive'}}</c-badge>
</td>
<td>{{item.desc_cron}}</td>
<td class="text-center">
<div class="d-flex gap-1 justify-content-center">
<button cButton color="warning" size="sm" (click)="editAddCloner(item,'edit');"
pTooltip="Edit Cloner">
<i class="fa-regular fa-pen-to-square"></i>
</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item);" pTooltip="Delete Cloner">
<i class="fa-regular fa-trash-can"></i>
</button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="6" class="text-center p-4">No cloners found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>
</c-row> </c-row>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<c-modal #EditClonerModal backdrop="static" size="lg" [(visible)]="EditClonerModalVisible" id="EditClonerModal"> <c-modal #EditClonerModal backdrop="static" size="lg" [(visible)]="EditClonerModalVisible" id="EditClonerModal">
<c-modal-header class="bg-light"> <c-modal-header class="bg-light">
<h5 *ngIf="SelectedCloner['action']=='edit'" cModalTitle><i class="fa-solid fa-edit me-2"></i>Edit Cloner: {{SelectedCloner['name']}}</h5> <h5 *ngIf="SelectedCloner['action']=='edit'" cModalTitle><i class="fa-solid fa-edit me-2"></i>Edit Cloner:
<h5 *ngIf="SelectedCloner['action']=='add'" cModalTitle><i class="fa-solid fa-plus me-2"></i>Add New Cloner</h5> {{SelectedCloner['name']}}</h5>
<button [cModalToggle]="EditClonerModal.id" cButtonClose></button> <h5 *ngIf="SelectedCloner['action']=='add'" cModalTitle><i class="fa-solid fa-plus me-2"></i>Add New Cloner</h5>
</c-modal-header> <button [cModalToggle]="EditClonerModal.id" cButtonClose></button>
<c-modal-body class="p-3"> </c-modal-header>
<!-- Basic Information --> <c-modal-body class="p-3">
<div class="cloner-form-section mb-3"> <!-- Basic Information -->
<div class="section-header mb-2"> <div class="cloner-form-section mb-3">
<h6 class="section-title mb-0"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6> <div class="section-header mb-2">
</div> <h6 class="section-title mb-0"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
<c-row class="g-2">
<c-col xs="12" md="6">
<input cFormControl placeholder="Cloner Name" [(ngModel)]="SelectedCloner['name']" class="form-input-sm" />
</c-col>
<c-col xs="12" md="6">
<input cFormControl placeholder="Description" [(ngModel)]="SelectedCloner['description']" class="form-input-sm" />
</c-col>
</c-row>
</div> </div>
<c-row class="g-2">
<c-col xs="12" md="6">
<input cFormControl placeholder="Cloner Name" [(ngModel)]="SelectedCloner['name']" class="form-input-sm" />
</c-col>
<c-col xs="12" md="6">
<input cFormControl placeholder="Description" [(ngModel)]="SelectedCloner['description']"
class="form-input-sm" />
</c-col>
</c-row>
</div>
<!-- Sync Configuration --> <!-- Sync Configuration -->
<div class="cloner-form-section mb-3"> <div class="cloner-form-section mb-3">
<div class="section-header mb-2"> <div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-sync me-2"></i>Synchronization Settings</h6> <h6 class="section-title mb-0"><i class="fa-solid fa-sync me-2"></i>Synchronization Settings</h6>
</div>
<c-row class="g-2">
<c-col xs="12" md="4">
<label class="form-label-xs">Direction</label>
<select cSelect [(ngModel)]="SelectedCloner['direction']" (change)="onDirectionChange()" class="form-select-xs">
<option value="twoway">Two Way Sync</option>
<option value="oneway">Master Mode</option>
</select>
</c-col>
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
<label class="form-label-xs">Live Mode</label>
<select cSelect [(ngModel)]="SelectedCloner['live_mode']" class="form-select-xs">
<option [ngValue]="false">Inactive</option>
<option [ngValue]="true">Active</option>
</select>
</c-col>
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
<label class="form-label-xs">Schedule</label>
<select cSelect [(ngModel)]="SelectedCloner['schedule']" class="form-select-xs">
<option [ngValue]="false">Inactive</option>
<option [ngValue]="true">Active</option>
</select>
</c-col>
<c-col xs="12" *ngIf="SelectedCloner['schedule'] && SelectedCloner['direction']=='oneway'">
<label class="form-label-xs">Cron Expression</label>
<input cFormControl placeholder="0 0 * * *" [(ngModel)]="SelectedCloner['cron']" class="form-input-sm" />
</c-col>
</c-row>
</div> </div>
<c-row class="g-2">
<c-col xs="12" md="4">
<label class="form-label-xs">Direction</label>
<select cSelect [(ngModel)]="SelectedCloner['direction']" (change)="onDirectionChange()"
class="form-select-xs">
<option value="twoway">Two Way Sync</option>
<option value="oneway">Master Mode</option>
</select>
</c-col>
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
<label class="form-label-xs">Live Mode</label>
<select cSelect [(ngModel)]="SelectedCloner['live_mode']" class="form-select-xs">
<option [ngValue]="false">Inactive</option>
<option [ngValue]="true">Active</option>
</select>
</c-col>
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
<label class="form-label-xs">Schedule</label>
<select cSelect [(ngModel)]="SelectedCloner['schedule']" class="form-select-xs">
<option [ngValue]="false">Inactive</option>
<option [ngValue]="true">Active</option>
</select>
</c-col>
<c-col xs="12" *ngIf="SelectedCloner['schedule'] && SelectedCloner['direction']=='oneway'">
<label class="form-label-xs">Cron Expression</label>
<input cFormControl placeholder="0 0 * * *" [(ngModel)]="SelectedCloner['cron']" class="form-input-sm" />
</c-col>
</c-row>
</div>
<!-- Peers Configuration (Currently disabled - only devices supported) --> <!-- Peers Configuration (Currently disabled - only devices supported) -->
<!-- <div class="cloner-form-section mb-3"> <!-- <div class="cloner-form-section mb-3">
<div class="section-header mb-2"> <div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-network-wired me-2"></i>Peers Configuration</h6> <h6 class="section-title mb-0"><i class="fa-solid fa-network-wired me-2"></i>Peers Configuration</h6>
<small class="text-muted">Select the type of peers to synchronize</small> <small class="text-muted">Select the type of peers to synchronize</small>
@ -129,233 +195,301 @@
</c-row> </c-row>
</div> --> </div> -->
<!-- Commands Configuration --> <!-- Commands Configuration -->
<div class="cloner-form-section mb-3"> <div class="cloner-form-section mb-3">
<div class="section-header mb-2"> <div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-terminal me-2"></i>Commands Configuration</h6> <h6 class="section-title mb-0"><i class="fa-solid fa-terminal me-2"></i>Commands Configuration</h6>
</div> </div>
<div class="commands-container-compact"> <div class="commands-container-compact">
<div class="nav nav-underline commands-nav-compact"> <div class="nav nav-underline commands-nav-compact">
<div class="nav-item" *ngFor="let tab of tabs; let i = index"> <div class="nav-item" *ngFor="let tab of tabs; let i = index">
<a class="nav-link" [active]="i==0" [cTabContent]="tabContent" [tabPaneIdx]="i">{{ tab.name }}</a> <a class="nav-link" [active]="i==0" [cTabContent]="tabContent" [tabPaneIdx]="i">{{ tab.name }}</a>
</div>
</div> </div>
<c-tab-content class="command-sections-compact" #tabContent="cTabContent"> </div>
<c-tab-pane *ngFor="let tab of tabs; let i = index"> <c-tab-content class="command-sections-compact" #tabContent="cTabContent">
<div class="command-category-compact" *ngFor="let section of tab.sections"> <c-tab-pane *ngFor="let tab of tabs; let i = index">
<h6 class="category-title-compact">{{ section.title }}</h6> <div class="command-category-compact" *ngFor="let section of tab.sections">
<div class="commands-grid-compact"> <h6 class="category-title-compact">{{ section.title }}</h6>
<div class="command-item-compact" *ngFor="let command of section.commands"> <div class="commands-grid-compact">
<div class="command-content-compact"> <div class="command-item-compact" *ngFor="let command of section.commands">
<span class="command-name-compact">{{ command }}</span> <div class="command-content-compact">
<div class="custom-switch-compact"> <span class="command-name-compact">{{ command }}</span>
<input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)" (click)="activate_command(command)" [id]="command.replace('/', '')"> <div class="custom-switch-compact">
<label class="custom-control-label" [for]="command.replace('/', '')"></label> <input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)"
</div> (click)="activate_command(command)" [id]="command.replace('/', '')">
<label class="custom-control-label" [for]="command.replace('/', '')"></label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</c-tab-pane> </div>
</c-tab-content> </c-tab-pane>
</c-tab-content>
</div>
</div>
<!-- Master Device Selection (for Master Mode) -->
<div class="cloner-form-section mb-2" *ngIf="SelectedCloner['direction']=='oneway' && SelectedMembers.length > 0">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-crown me-2 text-warning"></i>Master Device</h6>
</div>
<div class="master-selection-compact">
<div class="master-device-compact" *ngIf="master > 0">
<i class="fa-solid fa-server me-2 text-primary"></i>
<strong>{{getMasterDeviceName()}}</strong>
<c-badge color="warning" class="ms-2">Master</c-badge>
</div>
<div class="no-master-compact" *ngIf="master == 0">
<i class="fa-solid fa-exclamation-triangle me-2 text-warning"></i>
<span class="text-muted">No master device selected</span>
</div>
</div>
</div>
<!-- Device Management -->
<div class="cloner-form-section">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-server me-2"></i>Device Management</h6>
<div class="d-flex justify-content-between align-items-center">
<c-badge color="info">{{SelectedMembers.length}} device(s)</c-badge>
</div> </div>
</div> </div>
<!-- Master Device Selection (for Master Mode) --> <div class="peers-container">
<div class="cloner-form-section mb-2" *ngIf="SelectedCloner['direction']=='oneway' && SelectedMembers.length > 0"> <div *ngIf="SelectedMembers.length == 0" class="empty-peers">
<div class="section-header mb-2"> <div class="empty-icon">
<h6 class="section-title mb-0"><i class="fa-solid fa-crown me-2 text-warning"></i>Master Device</h6> <i class="fa-solid fa-users-slash fa-2x text-muted opacity-50"></i>
</div>
<div class="master-selection-compact">
<div class="master-device-compact" *ngIf="master > 0">
<i class="fa-solid fa-server me-2 text-primary"></i>
<strong>{{getMasterDeviceName()}}</strong>
<c-badge color="warning" class="ms-2">Master</c-badge>
</div> </div>
<div class="no-master-compact" *ngIf="master == 0"> <div class="empty-text">
<i class="fa-solid fa-exclamation-triangle me-2 text-warning"></i> <strong>No peers added</strong>
<span class="text-muted">No master device selected</span> <p class="mb-0 text-muted">Click "Add Members" to start adding peers</p>
</div>
</div>
</div>
<!-- Device Management -->
<div class="cloner-form-section">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-server me-2"></i>Device Management</h6>
<div class="d-flex justify-content-between align-items-center">
<c-badge color="info">{{SelectedMembers.length}} device(s)</c-badge>
</div> </div>
</div> </div>
<div class="peers-container"> <div *ngIf="SelectedMembers.length > 0">
<div *ngIf="SelectedMembers.length == 0" class="empty-peers"> <p-table [value]="SelectedMembers" [paginator]="true" [rows]="10" [showGridlines]="true" [stripedRows]="true"
<div class="empty-icon"> styleClass="p-datatable-sm">
<i class="fa-solid fa-users-slash fa-2x text-muted opacity-50"></i> <ng-template pTemplate="header">
</div> <tr>
<div class="empty-text"> <th pSortableColumn="name" pResizableColumn>
<strong>No peers added</strong> <div class="justify-between">
<p class="mb-0 text-muted">Click "Add Members" to start adding peers</p> <span>Name</span>
</div> <p-sortIcon field="name"></p-sortIcon>
</div>
<div *ngIf="SelectedMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[rowSelection]="rowSelection" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<div class="d-flex align-items-center">
<i class="fa-solid fa-crown me-2 text-warning" *ngIf="SelectedCloner['direction']=='oneway' && item.id==master" title="Master Device"></i>
<i class="fa-solid fa-server me-2 text-primary" *ngIf="!(SelectedCloner['direction']=='oneway' && item.id==master)"></i>
<strong>{{value}}</strong>
</div> </div>
</ng-template> </th>
</gui-grid-column> <th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="mac" pResizableColumn>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC" field="mac"> <div class="justify-between">
<ng-template let-value="item.mac" let-item="item" let-index="index"> <span>MAC Address</span>
<c-badge color="secondary">{{value}}</c-badge> <p-sortIcon field="mac"></p-sortIcon>
</ng-template> </div>
</gui-grid-column> </th>
<gui-grid-column header="Actions" width="120" field="action" align="center"> <th style="width: 100px" class="text-center" pResizableColumn>Actions</th>
<ng-template let-value="item.id" let-item="item" let-index="index"> </tr>
<div class="btn-group" role="group"> </ng-template>
<button *ngIf="SelectedCloner['direction']=='oneway'" cButton color="warning" size="sm" variant="outline"
[cTooltip]="'Set as Master'" (click)="set_master(item.id)" [disabled]="item.id==master"> <ng-template pTemplate="body" let-item>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="fa-solid fa-crown me-2 text-warning"
*ngIf="SelectedCloner['direction']=='oneway' && item.id==master" pTooltip="Master Device"></i>
<i class="fa-solid fa-server me-2 text-primary"
*ngIf="!(SelectedCloner['direction']=='oneway' && item.id==master)"></i>
<strong>{{item.name}}</strong>
</div>
</td>
<td *ngIf="SelectedCloner['pair_type']=='devices'">
<c-badge color="secondary">{{item.mac}}</c-badge>
</td>
<td class="text-center">
<div class="d-flex gap-1 justify-content-center">
<button *ngIf="SelectedCloner['direction']=='oneway'" cButton color="warning" size="sm"
variant="outline" pTooltip="Set as Master" (click)="set_master(item.id)"
[disabled]="item.id==master">
<i class="fa-solid fa-crown"></i> <i class="fa-solid fa-crown"></i>
</button> </button>
<button cButton color="danger" size="sm" variant="outline" <button cButton color="danger" size="sm" variant="outline" pTooltip="Remove Member"
[cTooltip]="'Remove Member'" (click)="remove_member(item)"> (click)="remove_member(item)">
<i class="fa-solid fa-times"></i> <i class="fa-solid fa-trash-can"></i>
</button> </button>
</div> </div>
</ng-template> </td>
</gui-grid-column> </tr>
</gui-grid> </ng-template>
</div> <ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="SelectedCloner['pair_type']=='devices' ? 3 : 2" class="text-center p-2">No members
added.</td>
</tr>
</ng-template>
</p-table>
</div>
<div class="add-members-section mt-3"> <div class="add-members-section mt-3">
<button cButton color="success" (click)="show_new_member_form()"> <button cButton color="success" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add Members <i class="fa-solid fa-plus me-1"></i>Add Members
</button> </button>
</div>
</div> </div>
</div> </div>
</c-modal-body> </div>
<c-modal-footer> </c-modal-body>
<button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button> <c-modal-footer>
<button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button> <button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
<button [cModalToggle]="EditClonerModal.id" cButton color="secondary"> <button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
Close <button [cModalToggle]="EditClonerModal.id" cButton color="secondary">
</button> Close
</c-modal-footer> </button>
</c-modal> </c-modal-footer>
</c-modal>
<c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal"> <c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<c-modal-header class="bg-success text-white"> <c-modal-header class="bg-success text-white">
<h5 cModalTitle><i class="fa-solid fa-user-plus me-2"></i>Add Members to Cloner</h5> <h5 cModalTitle><i class="fa-solid fa-user-plus me-2"></i>Add Members to Cloner</h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button> <button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header> </c-modal-header>
<c-modal-body class="p-4"> <c-modal-body class="p-4">
<!-- Selection Summary --> <!-- Selection Summary -->
<div class="mb-3" style="min-height: 58px;"> <div class="mb-3" style="min-height: 58px;">
<c-alert *ngIf="NewMemberRows.length > 0" color="info" class="d-flex align-items-center mb-0"> <c-alert *ngIf="NewMemberRows.length > 0" color="info" class="d-flex align-items-center mb-0">
<i class="fa-solid fa-info-circle me-2"></i> <i class="fa-solid fa-info-circle me-2"></i>
<span><strong>{{NewMemberRows.length}}</strong> {{SelectedCloner['pair_type']}} selected for addition</span> <span><strong>{{NewMemberRows.length}}</strong> {{SelectedCloner['pair_type']}} selected for addition</span>
</c-alert> </c-alert>
</div> </div>
<!-- Available Members --> <!-- Available Members -->
<c-card> <c-card>
<c-card-header class="bg-light"> <c-card-header class="bg-light">
<h6 class="mb-0"> <h6 class="mb-0">
<i class="fa-solid me-2" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'" [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i> <i class="fa-solid me-2" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'"
Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total) [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
</h6> Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total)
</c-card-header> </h6>
<c-card-body class="p-0"> </c-card-header>
<div *ngIf="availbleMembers.length == 0" class="text-center p-4 text-muted"> <c-card-body class="p-0">
<i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i> <div *ngIf="availbleMembers.length == 0" class="text-center p-4 text-muted">
<h6>All {{SelectedCloner['pair_type']}} are already added</h6> <i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i>
<p class="mb-0">No available {{SelectedCloner['pair_type']}} to add to this cloner</p> <h6>All {{SelectedCloner['pair_type']}} are already added</h6>
<p class="mb-0">No available {{SelectedCloner['pair_type']}} to add to this cloner</p>
</div>
<div *ngIf="availbleMembers.length > 0">
<div class="mb-3 d-flex justify-content-end p-2">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search available..."
(input)="applyFilterGlobalNewMembers($event, 'contains')" class="form-control-sm" />
</span>
</div> </div>
<div *ngIf="availbleMembers.length > 0">
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching" <p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [rowsPerPageOptions]="[10, 25, 50]" [showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true [(selection)]="SelectedNewMemberRows" (selectionChange)="onSelectedRowsNewMembers($event)"
[paging]="paging"> [globalFilterFields]="['name', 'ip', 'mac']">
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index"> <ng-template pTemplate="header">
<div class="d-flex align-items-center"> <tr>
<i class="fa-solid me-2 text-primary" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'" [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i> <th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
<strong>{{value}}</strong> <th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Name</span>
<p-sortIcon field="name"></p-sortIcon>
</div> </div>
</ng-template> </th>
</gui-grid-column> <th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="ip" pResizableColumn>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="IP Address" field="ip"> <div class="justify-between">
<ng-template let-value="item.ip" let-item="item" let-index="index"> <span>IP Address</span>
<c-badge color="secondary">{{value}}</c-badge> <p-sortIcon field="ip"></p-sortIcon>
</ng-template> </div>
</gui-grid-column> </th>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC Address" field="mac"> <th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="mac" pResizableColumn>
<ng-template let-value="item.mac" let-item="item" let-index="index"> <div class="justify-between">
<small class="text-muted">{{value}}</small> <span>MAC Address</span>
</ng-template> <p-sortIcon field="mac"></p-sortIcon>
</gui-grid-column> </div>
</gui-grid> </th>
</div> </tr>
</c-card-body> </ng-template>
</c-card>
</c-modal-body>
<c-modal-footer class="bg-light d-flex justify-content-between"> <ng-template pTemplate="body" let-item>
<div> <tr>
<small class="text-muted">Select {{SelectedCloner['pair_type']}} from the list above to add them to the cloner</small> <td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
</div> <td>
<div> <div class="d-flex align-items-center">
<button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success"> <i class="fa-solid me-2 text-primary" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'"
<i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} {{SelectedCloner['pair_type'] | titlecase}} [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
</button> <strong>{{item.name}}</strong>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2"> </div>
<i class="fa-solid fa-times me-1"></i>Cancel </td>
</button> <td *ngIf="SelectedCloner['pair_type']=='devices'">
</div> <c-badge color="secondary">{{item.ip}}</c-badge>
</c-modal-footer> </td>
</c-modal> <td *ngIf="SelectedCloner['pair_type']=='devices'">
<small class="text-muted">{{item.mac}}</small>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="SelectedCloner['pair_type']=='devices' ? 4 : 2" class="text-center p-4">No available
members.</td>
</tr>
</ng-template>
</p-table>
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal"> <c-modal-footer class="bg-light d-flex justify-content-between">
<c-modal-header> <div>
<h5 cModalTitle>Confirm delete {{ SelectedCloner['name'] }}</h5> <small class="text-muted">Select {{SelectedCloner['pair_type']}} from the list above to add them to the
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button> cloner</small>
</c-modal-header> </div>
<c-modal-body> <div>
Are you sure that You want to delete following task ? <button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success">
<br /> <i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} {{SelectedCloner['pair_type'] | titlecase}}
<br />
<table style="width: 100%;">
<tr>
<td><b>Taks name : </b></td>
<td>{{ SelectedCloner['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedCloner['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedCloner['desc_cron'] }}</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="confirm_delete('',true)" cButton color="danger">
Yes,Delete!
</button> </button>
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info"> <button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2">
Close <i class="fa-solid fa-times me-1"></i>Cancel
</button> </button>
</c-modal-footer> </div>
</c-modal> </c-modal-footer>
</c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedCloner['name'] }}</h5>
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
Are you sure that You want to delete following task ?
<br />
<br />
<table style="width: 100%;">
<tr>
<td><b>Taks name : </b></td>
<td>{{ SelectedCloner['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedCloner['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedCloner['desc_cron'] }}</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="confirm_delete('',true)" cButton color="danger">
Yes,Delete!
</button>
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info">
Close
</button>
</c-modal-footer>
</c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -1,19 +1,8 @@
import { Component, OnInit, OnDestroy, ViewChildren ,QueryList } from "@angular/core"; import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { NgxSuperSelectOptions } from "ngx-super-select"; import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
@ -27,10 +16,13 @@ import { AppToastComponent } from "../toast-simple/toast.component";
}) })
export class ClonerComponent implements OnInit { export class ClonerComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public ispro: boolean = false; public ispro: boolean = false;
@ViewChild('dt') table!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
private router: Router, private router: Router,
@ -62,7 +54,6 @@ export class ClonerComponent implements OnInit {
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public SelectedCloner: any = {}; public SelectedCloner: any = {};
@ -74,9 +65,17 @@ export class ClonerComponent implements OnInit {
public NewMemberModalVisible: boolean = false; public NewMemberModalVisible: boolean = false;
public availbleMembers: any = []; public availbleMembers: any = [];
public NewMemberRows: any = []; public NewMemberRows: any = [];
public SelectedNewMemberRows: any; public SelectedNewMemberRows: any[] = [];
public master: number = 0; public master: number = 0;
public active_commands:any=[]; public active_commands: any = [];
toasterForm = {
autohide: true,
delay: 3000,
position: "fixed",
fade: true,
closeButton: true,
};
public tabs:any=[ public tabs:any=[
{ {
"name": "Network", "name": "Network",
@ -186,59 +185,13 @@ export class ClonerComponent implements OnInit {
} }
]; ];
public sorting = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
multiSorting: true, }
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
options: Partial<NgxSuperSelectOptions> = { applyFilterGlobalNewMembers($event: any, stringVal: string) {
selectionMode: "single", this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
actionsEnabled: false, }
displayExpr: "name",
valueExpr: "id",
placeholder: "Snippet",
searchEnabled: true,
enableDarkMode: false,
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
toasterForm = {
autohide: true,
delay: 3000,
position: "fixed",
fade: true,
closeButton: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
activate_command(command:string){ activate_command(command:string){
// add to active_commands if it not added before // add to active_commands if it not added before
if(!this.active_commands.includes(command)){ if(!this.active_commands.includes(command)){
@ -340,9 +293,9 @@ export class ClonerComponent implements OnInit {
} }
} }
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void { onSelectedRowsNewMembers(rows: any[]): void {
this.NewMemberRows = rows; this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source); this.SelectedNewMemberRows = rows;
} }
add_new_members() { add_new_members() {

View file

@ -18,9 +18,12 @@ import {
} from "@coreui/angular"; } from "@coreui/angular";
import { ClonerRoutingModule } from "./cloner-routing.module"; import { ClonerRoutingModule } from "./cloner-routing.module";
import { ClonerComponent } from "./cloner.component"; import { ClonerComponent } from "./cloner.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { NgxSuperSelectModule} from "ngx-super-select"; import { NgxSuperSelectModule} from "ngx-super-select";
import { SharedModule } from "../../shared/shared.module";
@NgModule({ @NgModule({
imports: [ imports: [
@ -31,7 +34,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule, ModalModule,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
@ -42,6 +47,7 @@ import { NgxSuperSelectModule} from "ngx-super-select";
TabsModule, TabsModule,
BadgeModule, BadgeModule,
AlertModule, AlertModule,
SharedModule,
], ],
declarations: [ClonerComponent], declarations: [ClonerComponent],
providers: [TitleCasePipe], providers: [TitleCasePipe],

View file

@ -142,98 +142,179 @@
</form> </form>
</c-col> </c-col>
</c-row> </c-row>
<div *ngIf="!chart_data.datasets" style="height: 250px;"> <div *ngIf="!chart_data.datasets" style="height: 230px;">
<div class="placeholder-glow" style="height: 100%;"> <div class="placeholder-glow" style="height: 100%;">
<div class="placeholder col-12" style="height: 250px;"></div> <div class="placeholder col-12" style="height: 230px;"></div>
</div> </div>
</div> </div>
<c-chart *ngIf="chart_data.datasets" [data]="chart_data" [options]="options" [height]="250" type="line"> <c-chart *ngIf="chart_data.datasets" [data]="chart_data" [options]="options" [height]="230" type="line">
</c-chart> </c-chart>
</c-card-body> </c-card-body>
</c-card> </c-card>
<c-row> <c-row class="align-items-stretch">
<c-col xl="6" lg="12" class="h-100" style="min-height: 160px!important;display: grid"> <c-col xl="6" lg="12" class="d-flex flex-column mb-3 mb-xl-0" style="min-height: 160px!important;">
<c-card class="mb-1 p-1 h-100" style="padding-left: 5px!important;"> <c-card class="flex-grow-1 border-0 shadow-sm"
<div class="my-1"> style="margin: 0 !important; overflow: hidden; background: linear-gradient(to bottom right, #ffffff, #f8f9fa);">
<h4 style="display: inline-block;">Version and Serial information</h4> <c-card-body class="d-flex flex-column justify-content-between p-2 px-3">
</div> <!-- Skeleton loader -->
<div *ngIf="!stats"> <div *ngIf="!stats">
<div class="placeholder-glow"> <div class="placeholder-glow">
<div class="placeholder col-8"></div> <div class="placeholder col-8 mb-2"></div>
<div class="placeholder col-6"></div> <div class="placeholder col-6 mb-2"></div>
<div class="placeholder col-10"></div> <div class="placeholder col-10 mb-2"></div>
<div class="placeholder col-7"></div> <div class="placeholder col-7"></div>
</div>
</div> </div>
</div>
<div *ngIf="!stats['license']" class="my-1">
<div style="display: inline-block;margin-right: 5px;">
<code style="padding: 0!important;">Serial:</code> <small
style="background-color: #ccc;padding: 5px;border-radius: 5px;cursor: pointer;" (click)="copy_this()"
[cdkCopyToClipboard]="stats['serial']">{{ stats['serial'] }}</small>
<span *ngIf="copy_msg" style="color: #fff!important;" class="badge text-bg-success"><i
class="fa-solid fa-check"></i>Copy</span>
</div>
<c-badge *ngIf="stats['username']" color="danger">Not Registred</c-badge>
<c-badge *ngIf="!stats['username']" color="danger">License Validation failed</c-badge>
</div>
<div *ngIf="stats['license']=='connection_error'" class="my-1">
<div style="display: inline-block;margin-right: 5px;">
<code style="padding: 0!important;">Serial:</code> <small
style="background-color: #ccc;padding: 5px;border-radius: 5px;cursor: pointer;" (click)="copy_this()"
[cdkCopyToClipboard]="stats['serial']">{{ stats['serial'] }}</small>
<span *ngIf="copy_msg" style="color: #fff!important;" class="badge text-bg-success mx-1"><i
class="fa-solid fa-check"></i>Copy</span>
</div>
<c-badge class="mx-1" color="danger">Unable connect to server/Check server internet connection</c-badge>
</div>
<div *ngIf="stats['license']!='connection_error' && stats['license']" class="my-1">
<div style="display: inline-block;margin-right: 5px;">
<code style="padding: 0!important;">Serial:</code> <small
style="background-color: #ccc;padding: 5px;border-radius: 5px;cursor: pointer;" (click)="copy_this()"
[cdkCopyToClipboard]="stats['serial']">{{ stats['serial'] }}</small>
<span *ngIf="copy_msg" style="color: #fff!important;" class="badge text-bg-success mx-1"><i
class="fa-solid fa-check"></i>Copy</span>
</div>
<c-badge color="success">Registred</c-badge>
<c-badge class="mx-1" color="info">License Type : {{stats['license']}}</c-badge>
<c-badge *ngIf="stats['update_mode']!='auto'" color="info">Manual update</c-badge>
<c-badge *ngIf="stats['update_mode']=='auto'" color="info">Auto update</c-badge>
</div>
<div *ngIf="stats['license']!='connection_error'" class="my-1">
<span style="font-size: 0.9rem; display: inline-block;margin-right: 5px"><c-badge
[color]="stats['update_available'] ? 'success' : 'secondary'"
style="margin: 0!important;padding: 8px;height: 27px;">Your Mikroman version : {{stats['version']}}
</c-badge>
<i class="fa-solid fa-spinner fa-spin" *ngIf="stats['update_inprogress']"></i>
<button cButton color="warning"
*ngIf="stats['update_mode']!='auto' && stats['update_available'] && !stats['update_inprogress']" size="sm"
(click)="showConfirmModal('update_mikroman')"
style="font-size: 0.75em;position: relative;left: -4px;top: 1px;border-top-left-radius: 0;border-bottom-left-radius: 0;height: 27px;"><i
class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update availble </button>
</span>
<span style="font-size: 0.9rem; display: inline-block;"><c-badge
[color]="stats['front_update_available'] ? 'success' : 'secondary'" style="padding: 8px;height: 27px;"
color="secondary">Your Mikrofront version : {{front_version}}
</c-badge>
<i class="fa-solid fa-spinner fa-spin" *ngIf="stats['front_update_inprogress']"></i>
<button cButton color="warning"
*ngIf="stats['update_mode']!='auto' && stats['front_update_available'] && !stats['front_update_inprogress']"
size="sm" (click)="showConfirmModal('update_mikrofront')"
style="font-size: 0.75em;position: relative;left: -4px;top: 1px;border-top-left-radius: 0;border-bottom-left-radius: 0;height: 27px;"><i
class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update availble </button>
</span>
</div>
<p *ngIf="!stats['license'] && !stats['username']" style="color: rgb(0, 119, 255);"><strong>License User name is not set in settings <a style="color: rgb(0, 119, 255);" target="_blank" href="https://mikrowizard.com/docs/register-serial-number/" >read more!</a></strong></p>
<p *ngIf="!stats['license'] && stats['username']" style="color: rgb(0, 119, 255);"><strong>Serial number not submited<a style="color: rgb(0, 119, 255);" target="_blank" href="https://mikrowizard.com/docs/register-serial-number/" >read more!</a></strong> </p>
<!-- <div *ngIf="stats['update_mode']!='auto'" class="my-1"> <div *ngIf="stats" class="d-flex flex-column h-100">
<button cButton color="warning" *ngIf="stats['update_available']" size="sm" style="font-size: 1em;"><i class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update <strong>Mikroman</strong> and reload server</button> <!-- Header / Serial -->
<button cButton color="warning" *ngIf="stats['front_update_available']" size="sm" style="font-size: 1em;margin-left: 5px;"><i class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update <strong>MikroFront</strong> and reload Page</button> <div class="d-flex justify-content-between align-items-center mb-2">
</div> --> <div>
<h6 class="fw-bold mb-0 d-inline-block me-2" style="color: #2c384af0;">Version & License</h6>
<div class="text-muted d-inline-block align-items-center" style="font-size: 0.75rem;">
<span class="me-1 font-monospace fw-semibold">{{ stats['serial'] }}</span>
<button (click)="copy_this()" [cdkCopyToClipboard]="stats['serial']"
class="btn btn-sm btn-light py-0 px-2 border-0 rounded-pill shadow-none lh-1"
style="font-size: 0.7rem; background-color: #e2e8f0; color: #475569;">
<i class="fa-regular fa-copy"></i>
</button>
<span *ngIf="copy_msg" class="badge bg-success ms-1 fade-in" style="font-size: 0.65rem;"><i
class="fa-solid fa-check"></i></span>
</div>
</div>
<div>
<span class="badge rounded-pill fw-semibold shadow-sm lh-1"
[ngClass]="stats?.license_info?.status === 'connection_error' ? 'text-bg-secondary' : 'text-bg-success'"
style="font-size: 0.7rem; padding: 4px 8px;">
<i class="fa-solid me-1"
[ngClass]="stats?.license_info?.status === 'connection_error' ? 'fa-globe-slash' : 'fa-globe'"></i>
{{ stats?.update_mode === 'auto' ? 'Auto Updates On' : 'Manual Updates' }}
</span>
</div>
</div>
<!-- License Info Banner -->
<div class="p-2 mb-2 rounded-3 position-relative" [ngClass]="{
'bg-success bg-opacity-10 border-start border-4 border-success': stats?.license_info?.status === 'Pro' || stats?.license_info?.status === 'Free',
'bg-danger bg-opacity-10 border-start border-4 border-danger': stats?.license_info?.status === 'expired' || stats?.license_info?.status === 'connection_error',
'bg-warning bg-opacity-10 border-start border-4 border-warning': stats?.license_info?.action_required === 'set_username'
}" style="box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);">
<div class="d-flex justify-content-between align-items-center">
<strong class="mb-0 text-dark d-flex align-items-center" style="font-size: 0.9rem;">
<i class="fa-solid flex-shrink-0 me-2" [ngClass]="{
'fa-crown text-warning': stats?.license_info?.status === 'Pro',
'fa-leaf text-success': stats?.license_info?.status === 'Free',
'fa-circle-xmark text-danger': stats?.license_info?.status === 'expired',
'fa-triangle-exclamation text-danger': stats?.license_info?.status === 'connection_error',
'fa-user-xmark text-warning': stats?.license_info?.status === 'no_username'
}"></i>
<span *ngIf="stats?.license_info?.status === 'Pro'">Pro License Active</span>
<span *ngIf="stats?.license_info?.status === 'Free'">Free License Active</span>
<span *ngIf="stats?.license_info?.status === 'expired'">License Expired</span>
<span *ngIf="stats?.license_info?.status === 'connection_error'">Connection Error</span>
<span *ngIf="stats?.license_info?.status === 'no_username'">Registration Required</span>
</strong>
<span *ngIf="stats?.license_info?.status === 'Pro' || stats?.license_info?.status === 'Free'"
class="badge fw-bold shadow-sm"
[ngClass]="stats?.license_info?.status === 'Pro' ? 'bg-warning text-dark' : 'bg-secondary'">
{{ stats?.license_info?.status | uppercase }}
</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-1">
<p class="mb-0 text-dark opacity-100" style="font-size: 0.75rem; line-height: 1.2; font-weight: 500;">
{{ stats?.license_info?.message || (stats?.license_info?.status === 'connection_error' ? 'Cannot connect
to server to verify license and updates.' : 'License information unavailable.') }}
</p>
<div
*ngIf="stats?.license_info?.action_required !== 'none' && stats?.license_info?.status !== 'connection_error'">
<a *ngIf="stats?.license_info?.action_required === 'set_username'"
href="https://mikrowizard.com/docs/register-serial-number/" target="_blank"
class="btn btn-sm btn-warning text-dark py-0 px-2 rounded-pill fw-semibold shadow-sm"
style="font-size: 0.7rem;"><i class="fa-solid fa-arrow-up-right-from-square me-1"></i> Setup</a>
<a *ngIf="stats?.license_info?.action_required === 'renew_license'"
href="https://mikrowizard.com/pricing" target="_blank"
class="btn btn-sm btn-danger py-0 px-2 rounded-pill fw-semibold shadow-sm"
style="font-size: 0.7rem;"><i class="fa-solid fa-cart-shopping me-1"></i> Renew</a>
</div>
</div>
</div>
<!-- Update Modules -->
<div class="mt-auto row g-2" *ngIf="stats?.license_info?.status !== 'connection_error'">
<div class="col-12 col-xl-6">
<div
class="p-1 px-2 border rounded-3 bg-white shadow-sm d-flex justify-content-between align-items-center h-100"
[ngClass]="{'border-success border-2 bg-success bg-opacity-10': stats['update_available']}">
<div>
<div class="fw-bold text-dark mb-0 d-flex align-items-center lh-1" style="font-size: 0.75rem;">
<i class="fa-solid fa-server me-1 text-secondary"></i>Mikroman Backend v{{stats['version']}}
</div>
<div class="text-muted lh-1 mt-1" style="font-size: 0.65rem;" *ngIf="!stats['update_available']">Up to
date</div>
<div class="text-success fw-bold lh-1 mt-1" style="font-size: 0.65rem;"
*ngIf="stats['update_available']"><i
class="fa-solid fa-arrow-up me-1"></i>v{{stats['latest_version']}} Available</div>
</div>
<div class="text-end">
<span *ngIf="stats['update_mode'] == 'auto' && stats['update_available']"
class="text-muted small fw-semibold" style="font-size: 0.65rem;"><i
class="fa-solid fa-clock me-1"></i>Auto-updating</span>
<button class="btn btn-sm btn-success rounded-pill fw-semibold py-0 px-2 shadow lh-1"
*ngIf="stats['update_mode']!='auto' && stats['update_available'] && !stats['update_inprogress']"
(click)="showConfirmModal('update_mikroman')"
style="font-size: 0.65rem; background: linear-gradient(45deg, #198754, #20c997); border: none; height: 20px;">
<i class="fa-solid fa-download me-1"></i> Update
</button>
<i class="fa-solid fa-circle-notch fa-spin text-success" style="font-size: 0.8rem;"
*ngIf="stats['update_inprogress']"></i>
</div>
</div>
</div>
<div class="col-12 col-xl-6">
<div
class="p-1 px-2 border rounded-3 bg-white shadow-sm d-flex justify-content-between align-items-center h-100"
[ngClass]="{'border-success border-2 bg-success bg-opacity-10': stats['front_update_available']}">
<div>
<div class="fw-bold text-dark mb-0 d-flex align-items-center lh-1" style="font-size: 0.75rem;">
<i class="fa-brands fa-angular me-1 text-secondary"></i>MikroFront UI v{{front_version}}
</div>
<div class="text-muted lh-1 mt-1" style="font-size: 0.65rem;"
*ngIf="!stats['front_update_available']">Up to date</div>
<div class="text-success fw-bold lh-1 mt-1" style="font-size: 0.65rem;"
*ngIf="stats['front_update_available']"><i
class="fa-solid fa-arrow-up me-1"></i>v{{stats['front_latest_version']}} Available</div>
</div>
<div class="text-end">
<span *ngIf="stats['update_mode'] == 'auto' && stats['front_update_available']"
class="text-muted small fw-semibold" style="font-size: 0.65rem;"><i
class="fa-solid fa-clock me-1"></i>Auto-updating</span>
<button class="btn btn-sm btn-success rounded-pill fw-semibold py-0 px-2 shadow lh-1"
*ngIf="stats['update_mode']!='auto' && stats['front_update_available'] && !stats['front_update_inprogress']"
(click)="showConfirmModal('update_mikrofront')"
style="font-size: 0.65rem; background: linear-gradient(45deg, #198754, #20c997); border: none; height: 20px;">
<i class="fa-solid fa-download me-1"></i> Update
</button>
<i class="fa-solid fa-circle-notch fa-spin text-success" style="font-size: 0.8rem;"
*ngIf="stats['front_update_inprogress']"></i>
</div>
</div>
</div>
</div>
<div *ngIf="stats?.license_info?.status === 'connection_error'" class="mt-auto py-1 text-center text-muted">
<div class="small fw-semibold opacity-50" style="font-size: 0.75rem;"><i
class="fa-solid fa-network-wired me-1"></i>Local functionality only. Remote services unreachable.</div>
</div>
</div>
</c-card-body>
</c-card> </c-card>
</c-col> </c-col>
<c-col xl="6" lg="12" class="h-100" style="min-height: 160px!important;display: grid;"> <c-col xl="6" lg="12" class="d-flex flex-column" style="min-height: 160px!important;">
<c-card class="h-100" style="padding: 0!important;margin: 0!important;"> <c-card class="flex-grow-1" style="padding: 0!important;margin: 0!important;">
<div *ngIf="!stats" style="padding: 20px;"> <div *ngIf="!stats" style="padding: 20px;">
<div class="placeholder-glow"> <div class="placeholder-glow">
<div class="placeholder col-4" style="height: 150px; float: left; margin-right: 20px;"></div> <div class="placeholder col-4" style="height: 150px; float: left; margin-right: 20px;"></div>

View file

@ -12,7 +12,7 @@
<tr> <tr>
<th scope="col">#</th> <th scope="col">#</th>
<th scope="col">Address</th> <th scope="col">Address</th>
<th scope="col">Att</th> <th scope="col">At</th>
<th scope="col">Group</th> <th scope="col">Group</th>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col">Via</th> <th scope="col">Via</th>
@ -27,8 +27,9 @@
<td>{{item['group']}}</td> <td>{{item['group']}}</td>
<td>{{item['name']}}</td> <td>{{item['name']}}</td>
<td>{{item['via']}}</td> <td>{{item['via']}}</td>
<td><button cButton (click)="killsession(item)" style="padding: 0;" size="sm" color="danger" variant="ghost"><i class="fa-solid fa-skull-crossbones"></i> <td><button cButton (click)="killsession(item)" style="padding: 0;" size="sm" color="danger"
kill</button></td> variant="ghost"><i class="fa-solid fa-skull-crossbones"></i>
kill</button></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -104,69 +104,97 @@
<c-card class="mb-1"> <c-card class="mb-1">
<c-card-body> <c-card-body>
<c-row style="flex-direction: row"> <c-row style="flex-direction: row">
<gui-grid [source]="interfaces" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <div class="mb-3 d-flex justify-content-end w-100">
[autoResizeWidth]="true"> <span class="p-input-icon-left">
<gui-grid-column header="Name" field="name"> <i class="pi pi-search"></i>
<ng-template let-value="item.name" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search interfaces..." (input)="applyFilterGlobal($event, 'contains')"
{{ value }} - {{ item["default-name"] }} class="form-control-sm" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="MAC" field="mac-address"> <p-table #dtInterfaces [value]="interfaces" [paginator]="true" [rows]="10"
<ng-template let-value="item['mac-address']" let-item="item" let-index="index"> [showGridlines]="true" [stripedRows]="true"
{{ value }} [resizableColumns]="true" columnResizeMode="expand"
</ng-template> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</gui-grid-column> [globalFilterFields]="['name', 'default-name', 'mac-address']">
<gui-grid-column header="rx" field="rx-byte"> <ng-template pTemplate="header">
<ng-template let-value="item['rx-byte']" let-item="item" let-index="index"> <tr>
<div>{{ convert_bw_human(value, "rx") }}</div> <th pSortableColumn="name" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>Name</span>
<gui-grid-column header="tx" field="tx-byte"> <span>
<ng-template let-value="item['tx-byte']" let-item="item" let-index="index"> <p-sortIcon field="name"></p-sortIcon>
{{ convert_bw_human(value, "tx") }} <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="l2mtu" field="l2mtu"> </th>
<ng-template let-value="item.l2mtu" let-item="item" let-index="index"> <th pSortableColumn="mac-address" pResizableColumn>
curr:{{ value }}<br /> <div class="justify-between">
max : {{ item["max-l2mtu"] }} <span>MAC</span>
</ng-template> <span>
</gui-grid-column> <p-sortIcon field="mac-address"></p-sortIcon>
<gui-grid-column header="rx/s" field="rx-bits-per-second" [enabled]="false"> <p-columnFilter type="text" field="mac-address" display="menu" class="ms-auto" />
<ng-template let-value="item['rx-bits-per-second']" let-item="item" let-index="index"> </span>
{{ convert_bw_human(value, "rx") }} </div>
</ng-template> </th>
</gui-grid-column> <th pSortableColumn="rx-byte" pResizableColumn>
<gui-grid-column header="tx/s" field="tx-bits-per-second" [enabled]="false"> <div class="justify-between">
<ng-template let-value="item['tx-bits-per-second']" let-item="item" let-index="index"> <span>RX</span>
{{ convert_bw_human(value, "tx") }} <span>
</ng-template> <p-sortIcon field="rx-byte"></p-sortIcon>
</gui-grid-column> <p-columnFilter type="numeric" field="rx-byte" display="menu" class="ms-auto" />
<gui-grid-column header="Created" field="created" [enabled]="false"> </span>
<ng-template let-value="item.created" let-item="item.id" let-index="index"> </div>
{{ value }} </th>
</ng-template> <th pSortableColumn="tx-byte" pResizableColumn>
</gui-grid-column> <div class="justify-between">
<gui-grid-column header="Last Up" field="last-link-up-time"> <span>TX</span>
<ng-template let-value="item['last-link-up-time']" let-item="item" let-index="index"> <span>
{{ value }} <p-sortIcon field="tx-byte"></p-sortIcon>
</ng-template> <p-columnFilter type="numeric" field="tx-byte" display="menu" class="ms-auto" />
</gui-grid-column> </span>
<gui-grid-column header="Created" field="created" [enabled]="false"> </div>
<ng-template let-value="item.created" let-item="item.id" let-index="index"> </th>
{{ value }} <th pSortableColumn="l2mtu" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>L2MTU</span>
<gui-grid-column header="Actions" field="action" width="60" align="center"> <span>
<ng-template let-value="item.id" let-item="item" let-index="index"> <p-sortIcon field="l2mtu"></p-sortIcon>
<button cButton color="info" size="sm" (click)="show_interface_rate(item['default-name'])" <p-columnFilter type="numeric" field="l2mtu" display="menu" class="ms-auto" />
class="mx-1"> </span>
</div>
</th>
<th pSortableColumn="last-link-up-time" pResizableColumn>
<div class="justify-between">
<span>Last Up</span>
<span>
<p-sortIcon field="last-link-up-time"></p-sortIcon>
<p-columnFilter type="text" field="last-link-up-time" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 60px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-iface>
<tr>
<td>{{iface.name}} <small class="text-muted">({{iface['default-name']}})</small></td>
<td><small class="font-monospace">{{iface['mac-address']}}</small></td>
<td>{{convert_bw_human(iface['rx-byte'], 'rx')}}</td>
<td>{{convert_bw_human(iface['tx-byte'], 'tx')}}</td>
<td>
<small>curr: {{iface.l2mtu}}</small><br/>
<small>max: {{iface['max-l2mtu']}}</small>
</td>
<td><small>{{iface['last-link-up-time']}}</small></td>
<td class="text-center">
<button cButton color="info" size="sm" (click)="show_interface_rate(iface['default-name'])" pTooltip="Interface Chart">
<i class="fa-solid fa-chart-line"></i> <i class="fa-solid fa-chart-line"></i>
</button> </button>
</ng-template> </td>
</gui-grid-column> </tr>
</gui-grid> </ng-template>
</p-table>
</c-row> </c-row>
</c-card-body> </c-card-body>
</c-card> </c-card>
@ -182,16 +210,16 @@
</c-card-header> </c-card-header>
<c-card-body> <c-card-body>
<h6>{{ raddata.key }}</h6> <h6>{{ raddata.key }}</h6>
<app-widgets-dropdown [devicedata]="raddata.value"></app-widgets-dropdown> <app-widgets-dropdown [devicedata]="$any(raddata.value)"></app-widgets-dropdown>
<c-row> <c-row>
<c-col md="3"> <c-col md="3">
<table style="word-break: break-word" small stripedColumns cTable> <table style="word-break: break-word" small stripedColumns cTable>
<tbody> <tbody>
<ng-container *ngFor=" <ng-container *ngFor="
let d of raddata.value['data'] | keyvalue; let d of $any(raddata.value)['data'] | keyvalue;
let i = index let i = index
"> ">
<tr *ngIf="i < objectlen(raddata.value['data']) / 4"> <tr *ngIf="i < objectlen($any(raddata.value)['data']) / 4">
<th style="width: 20%; text-wrap: nowrap"> <th style="width: 20%; text-wrap: nowrap">
{{ d.key }} {{ d.key }}
</th> </th>
@ -205,12 +233,12 @@
<table style="word-break: break-word" small stripedColumns cTable> <table style="word-break: break-word" small stripedColumns cTable>
<tbody> <tbody>
<ng-container *ngFor=" <ng-container *ngFor="
let d of raddata.value['data'] | keyvalue; let d of $any(raddata.value)['data'] | keyvalue;
let i = index let i = index
"> ">
<tr *ngIf=" <tr *ngIf="
i >= objectlen(raddata.value['data']) / 4 && i >= objectlen($any(raddata.value)['data']) / 4 &&
i < (objectlen(raddata.value['data']) / 4) * 2 i < (objectlen($any(raddata.value)['data']) / 4) * 2
"> ">
<th style="width: 20%; text-wrap: nowrap"> <th style="width: 20%; text-wrap: nowrap">
{{ d.key }} {{ d.key }}
@ -225,12 +253,12 @@
<table style="word-break: break-word" small stripedColumns cTable> <table style="word-break: break-word" small stripedColumns cTable>
<tbody> <tbody>
<ng-container *ngFor=" <ng-container *ngFor="
let d of raddata.value['data'] | keyvalue; let d of $any(raddata.value)['data'] | keyvalue;
let i = index let i = index
"> ">
<tr *ngIf=" <tr *ngIf="
i >= (objectlen(raddata.value['data']) / 4) * 2 && i >= (objectlen($any(raddata.value)['data']) / 4) * 2 &&
i < (objectlen(raddata.value['data']) / 4) * 3 i < (objectlen($any(raddata.value)['data']) / 4) * 3
"> ">
<th style="width: 20%; text-wrap: nowrap"> <th style="width: 20%; text-wrap: nowrap">
{{ d.key }} {{ d.key }}
@ -245,11 +273,11 @@
<table small stripedColumns cTable> <table small stripedColumns cTable>
<tbody> <tbody>
<ng-container *ngFor=" <ng-container *ngFor="
let d of raddata.value['data'] | keyvalue; let d of $any(raddata.value)['data'] | keyvalue;
let i = index let i = index
"> ">
<tr *ngIf=" <tr *ngIf="
i >= (objectlen(raddata.value['data']) / 4) * 3 i >= (objectlen($any(raddata.value)['data']) / 4) * 3
"> ">
<th>{{ d.key }}</th> <th>{{ d.key }}</th>
<td scope="row">{{ d.value }}</td> <td scope="row">{{ d.value }}</td>
@ -259,7 +287,7 @@
</table> </table>
</c-col> </c-col>
</c-row> </c-row>
<c-row *ngIf="raddata.value['strength-at-rates']"> <c-row *ngIf="$any(raddata.value)['strength-at-rates']">
<c-col> <c-col>
<table style="word-break: break-word" small borderless cTable> <table style="word-break: break-word" small borderless cTable>
<tbody> <tbody>
@ -276,7 +304,7 @@
<td scope="row"> <td scope="row">
<c-badge color="info" style="font-size: 0.85em" class="mx-1" *ngFor=" <c-badge color="info" style="font-size: 0.85em" class="mx-1" *ngFor="
let st of strangth_at_rate_extract( let st of strangth_at_rate_extract(
raddata.value['strength-at-rates'] $any(raddata.value)['strength-at-rates']
) )
">{{ st }}</c-badge> ">{{ st }}</c-badge>
</td> </td>
@ -292,8 +320,7 @@
</c-tab-pane> </c-tab-pane>
<c-tab-pane> <c-tab-pane>
<ng-container *ngIf="dhcp_server_available"> <ng-container *ngIf="dhcp_server_available">
<app-dhcp-info [tz]="tz" [dhcp_server_data]="dhcp_server_data" [small_screen]="small_screen" [columnMenu]="columnMenu" <app-dhcp-info [tz]="tz" [dhcp_server_data]="dhcp_server_data" [small_screen]="small_screen"></app-dhcp-info>
[sorting]="sorting" [searching]="searching" [infoPanel]="infoPanel" [paging]="paging"></app-dhcp-info>
</ng-container> </ng-container>
</c-tab-pane> </c-tab-pane>
<c-tab-pane style="width: 100%;padding-top: 10px;background-color: #fff;"> <c-tab-pane style="width: 100%;padding-top: 10px;background-color: #fff;">

View file

@ -24,9 +24,6 @@
cursor: pointer; cursor: pointer;
} }
.interfaces gui-grid gui-structure{
min-height: unset!important;
}
.nav-underline .nav-link:hover,.nav-underline .nav-link:focus { .nav-underline .nav-link:hover,.nav-underline .nav-link:focus {
border-color: var(--cui-nav-underline-link-active-border-color, #321fdb) border-color: var(--cui-nav-underline-link-active-border-color, #321fdb)
} }

View file

@ -1,18 +1,8 @@
import { Component, OnDestroy, OnInit ,ViewEncapsulation} from "@angular/core"; import { Component, OnDestroy, OnInit, ViewEncapsulation, ViewChild } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { __setFunctionName } from "tslib"; import { __setFunctionName } from "tslib";
interface IUser { interface IUser {
name: string; name: string;
@ -38,10 +28,10 @@ type radiodata = {
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class DeviceComponent implements OnInit, OnDestroy { export class DeviceComponent implements OnInit, OnDestroy {
public uid: number; public uid!: number;
public sessionloaded: boolean = false; public sessionloaded: boolean = false;
public uname: string; public uname!: string;
public tz: string; public tz!: string;
public ispro: boolean = false; public ispro: boolean = false;
public small_screen=false; public small_screen=false;
public show_dev_logs: boolean = false; public show_dev_logs: boolean = false;
@ -81,8 +71,11 @@ export class DeviceComponent implements OnInit, OnDestroy {
} }
public devdata: any; public devdata: any;
public devsensors: any; public devsensors: any;
public radio_devsensors: radiodata; public radiodata!: radiodata;
public columns: Array<GuiColumn> = []; public radio_devsensors!: any;
@ViewChild('dtInterfaces') dtInterfaces!: Table;
public loading: boolean = true; public loading: boolean = true;
public radio_loading: boolean = true; public radio_loading: boolean = true;
public InterfaceChartModalVisible: boolean = false; public InterfaceChartModalVisible: boolean = false;
@ -98,45 +91,11 @@ export class DeviceComponent implements OnInit, OnDestroy {
public dhcp_server_available: boolean = false; public dhcp_server_available: boolean = false;
public dhcp_server_data: any = {}; public dhcp_server_data: any = {};
public sorting = {
enabled: true,
multiSorting: true,
};
public interfaces: Array<any> = []; public interfaces: Array<any> = [];
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 20,
pageSizes: [20],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.dtInterfaces.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
sort: true,
columnsManager: true,
};
objectlen(object:any){
return Object.keys(object).length;
} }
strangth_at_rate_extract(data:string){
return data.split(',');
}
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search In table",
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
reload_dhcp_server(){ reload_dhcp_server(){
this.get_DHCP_data(); this.get_DHCP_data();
} }
@ -553,6 +512,12 @@ export class DeviceComponent implements OnInit, OnDestroy {
show_history(itme:any) { show_history(itme:any) {
return return
} }
objectlen(object:any){
return object ? Object.keys(object).length : 0;
}
strangth_at_rate_extract(data:string){
return data ? data.split(',') : [];
}
ngOnDestroy() { ngOnDestroy() {
clearInterval(this.data_interval); clearInterval(this.data_interval);
} }

View file

@ -22,7 +22,9 @@ import { ChartjsModule } from "@coreui/angular-chartjs";
import { DeviceRoutingModule } from "./device-routing.module"; import { DeviceRoutingModule } from "./device-routing.module";
import { DeviceComponent } from "./device.component"; import { DeviceComponent } from "./device.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { WidgetsModule } from "../widgets/widgets.module"; import { WidgetsModule } from "../widgets/widgets.module";
import { DeviceInfoModule } from "./device-info/device-info.module"; import { DeviceInfoModule } from "./device-info/device-info.module";
@ -53,7 +55,9 @@ import { AccModule } from "../acc_log/acc.module";
AuthModule, AuthModule,
AccModule, AccModule,
SpinnerModule, SpinnerModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
NavbarModule, NavbarModule,
ModalModule, ModalModule,
TableModule, TableModule,

View file

@ -1,5 +1,5 @@
<ng-container *ngFor="let item of dhcp_server_data "> <ng-container *ngFor="let item of dhcp_server_data ">
<c-row style="padding: 10px 0;" > <c-row style="padding: 10px 0;">
<c-col xl="3" sm="12"> <c-col xl="3" sm="12">
<c-row *ngIf="small_screen"> <c-row *ngIf="small_screen">
<c-col sm="6" style="padding-right: 0;"> <c-col sm="6" style="padding-right: 0;">
@ -77,51 +77,101 @@
<c-col xl="9" sm="12"> <c-col xl="9" sm="12">
<c-card class="mb-1 h-100"> <c-card class="mb-1 h-100">
<c-card-body> <c-card-body>
<gui-grid [source]="item['lease_table']" [columnMenu]="columnMenu" [sorting]="sorting" <div class="mb-3 d-flex justify-content-end">
[searching]="searching" [infoPanel]="infoPanel" [rowHeight]="28" [autoResizeWidth]=true [paging]="paging"> <span class="p-input-icon-left table-search-input">
<gui-grid-column header="address" field="address"> <i class="pi pi-search"></i>
<ng-template let-value="item.address" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search leases..."
{{value}} (input)="applyFilterGlobalLeases($event, 'contains')" class="form-control-sm" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="Type" field="dynamic">
<ng-template let-value="item" let-item="item" let-index="index"> <p-table #dtLeases [value]="item['lease_table']" [paginator]="true" [rows]="10" [showGridlines]="true"
<span *ngIf="item['dynamic']">Dynamic</span> [stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
<span *ngIf="item['static']">Static</span> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</ng-template> [globalFilterFields]="['address', 'host-name', 'mac-address', 'status']">
</gui-grid-column> <ng-template pTemplate="header">
<gui-grid-column header="expire" field="expires-after"> <tr>
<ng-template let-value="item['expires-after']" let-item="item" let-index="index"> <th pSortableColumn="address" pResizableColumn>
{{value}} <div class="justify-between">
</ng-template> <span>Address</span>
</gui-grid-column> <span>
<gui-grid-column header="host-name" field="host-name"> <p-sortIcon field="address"></p-sortIcon>
<ng-template let-value="item['host-name']" let-item="item" let-index="index"> <p-columnFilter type="text" field="address" display="menu" class="ms-auto" />
{{value}} </span>
</ng-template> </div>
</gui-grid-column> </th>
<gui-grid-column header="last-seen" field="last-seen"> <th pSortableColumn="dynamic" pResizableColumn>
<ng-template let-value="item['last-seen']" let-item="item" let-index="index"> <div class="justify-between">
{{value}} <span>Type</span>
</ng-template> <span>
</gui-grid-column> <p-sortIcon field="dynamic"></p-sortIcon>
<gui-grid-column header="status" field="status"> <p-columnFilter type="boolean" field="dynamic" display="menu" class="ms-auto" />
<ng-template let-value="item['status']" let-item="item" let-index="index"> </span>
{{value}} </div>
</ng-template> </th>
</gui-grid-column> <th pSortableColumn="expires-after" pResizableColumn>
<gui-grid-column header="MAC" field="mac-address"> <div class="justify-between">
<ng-template let-value="item['mac-address']" let-item="item" let-index="index"> <span>Expire</span>
{{value}} <span>
</ng-template> <p-sortIcon field="expires-after"></p-sortIcon>
</gui-grid-column> <p-columnFilter type="text" field="expires-after" display="menu" class="ms-auto" />
<gui-grid-column header="Actions" width="60" field=""> </span>
<ng-template let-value="item" let-item="item" let-index="index"> </div>
<button cButton color="info" size="sm" (click)="show_history(item)" class="mx-1"><i </th>
class="fa-solid fa-clock-rotate-left"></i></button> <th pSortableColumn="host-name" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>Host Name</span>
</gui-grid> <span>
<p-sortIcon field="host-name"></p-sortIcon>
<p-columnFilter type="text" field="host-name" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="status" pResizableColumn>
<div class="justify-between">
<span>Status</span>
<span>
<p-sortIcon field="status"></p-sortIcon>
<p-columnFilter type="text" field="status" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="mac-address" pResizableColumn>
<div class="justify-between">
<span>MAC</span>
<span>
<p-sortIcon field="mac-address"></p-sortIcon>
<p-columnFilter type="text" field="mac-address" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 60px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-lease>
<tr>
<td>{{lease.address}}</td>
<td>
<c-badge [color]="lease.dynamic ? 'info' : 'secondary'">
{{lease.dynamic ? 'Dynamic' : 'Static'}}
</c-badge>
</td>
<td>{{lease['expires-after']}}</td>
<td>{{lease['host-name']}}</td>
<td>
<c-badge [color]="lease.status === 'bound' ? 'success' : 'warning'">
{{lease.status}}
</c-badge>
</td>
<td><small class="font-monospace">{{lease['mac-address']}}</small></td>
<td class="text-center">
<button cButton color="info" size="sm" (click)="show_history(lease)" pTooltip="View History">
<i class="fa-solid fa-clock-rotate-left"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>
@ -129,36 +179,60 @@
</ng-container> </ng-container>
<c-modal #DhcpHistoryModal backdrop="static" size="xl" [visible]="dhcp_history_modal" <c-modal #DhcpHistoryModal backdrop="static" size="xl" [visible]="dhcp_history_modal" id="DhcpHistoryModal"
id="DhcpHistoryModal" *ngIf="dhcp_history_modal"> *ngIf="dhcp_history_modal">
<c-modal-header> <c-modal-header>
<h5 cModalTitle>History for {{current_dhcp['mac-address']}}</h5> <h5 cModalTitle>History for {{current_dhcp['mac-address']}}</h5>
<button [cModalToggle]="DhcpHistoryModal.id" cButtonClose></button> <button [cModalToggle]="DhcpHistoryModal.id" cButtonClose></button>
</c-modal-header> </c-modal-header>
<c-modal-body style="overflow-x: auto;"> <c-modal-body style="overflow-x: auto;">
<gui-grid style="min-width: 900px;" [source]="dhcp_history" [columnMenu]="columnMenu" [sorting]="sorting" <div class="mb-3 d-flex justify-content-end">
[searching]="searching" [infoPanel]="infoPanel" [rowHeight]="35" [autoResizeWidth]=true [paging]="paging"> <span class="p-input-icon-left">
<gui-grid-column header="comment" field="comment" > <i class="pi pi-search"></i>
<ng-template let-value="item.comment" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search history..."
<div style="text-wrap: auto;font-weight: bold;line-height: normal;">{{value}}</div> (input)="applyFilterGlobalHistory($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dtHistory [value]="dhcp_history" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['comment', 'detail', 'eventtime', 'ip', 'name']">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="comment" pResizableColumn>
Comment
<p-sortIcon field="comment"></p-sortIcon>
<p-columnFilter type="text" field="comment" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="detail" style="width: 150px" pResizableColumn>
Type
<p-sortIcon field="detail"></p-sortIcon>
<p-columnFilter type="text" field="detail" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="eventtime" style="width: 200px" pResizableColumn>
Event Time
<p-sortIcon field="eventtime"></p-sortIcon>
<p-columnFilter type="text" field="eventtime" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="ip" style="width: 250px" pResizableColumn>
Router
<p-sortIcon field="ip"></p-sortIcon>
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
</th>
</tr>
</ng-template> </ng-template>
</gui-grid-column> <ng-template pTemplate="body" let-hist>
<gui-grid-column header="type" field="detail" width="150"> <tr>
<ng-template let-value="item['detail']" let-item="item" let-index="index"> <td>
{{value}} <div style="text-wrap: auto; font-weight: bold;">{{hist.comment}}</div>
</td>
<td>{{hist.detail}}</td>
<td>{{hist.eventtime}}</td>
<td>{{hist.name}} ({{hist.ip}})</td>
</tr>
</ng-template> </ng-template>
</gui-grid-column> </p-table>
<gui-grid-column header="eventtime" field="eventtime" width="200">
<ng-template let-value="item['eventtime']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Router" field="ip" width="250">
<ng-template let-value="item['ip']" let-item="item" let-index="index">
{{item.name}} ({{value}})
</ng-template>
</gui-grid-column>
</gui-grid>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
<button [cModalToggle]="DhcpHistoryModal.id" cButton color="secondary"> <button [cModalToggle]="DhcpHistoryModal.id" cButton color="secondary">

View file

@ -1,6 +1,7 @@
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core'; import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, ViewChild } from '@angular/core';
import { dataProvider } from "../../../providers/mikrowizard/data"; import { dataProvider } from "../../../providers/mikrowizard/data";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { Table } from 'primeng/table';
@Component({ @Component({
selector: 'app-dhcp-info', selector: 'app-dhcp-info',
@ -11,13 +12,11 @@ import { formatInTimeZone } from "date-fns-tz";
export class DhcpInfoComponent implements AfterContentInit { export class DhcpInfoComponent implements AfterContentInit {
@Input() dhcp_server_data: any; @Input() dhcp_server_data: any;
@Input() small_screen: boolean=false; @Input() small_screen: boolean=false;
@Input() columnMenu: any;
@Input() sorting: any;
@Input() searching: any;
@Input() infoPanel: any;
@Input() paging: any;
@Input() rowSelectionMode: any;
@Input() tz: any; @Input() tz: any;
@ViewChild('dtLeases') dtLeases!: Table;
@ViewChild('dtHistory') dtHistory!: Table;
dhcp_history:any; dhcp_history:any;
dhcp_history_modal: boolean = false; dhcp_history_modal: boolean = false;
current_dhcp:any; current_dhcp:any;
@ -26,7 +25,14 @@ export class DhcpInfoComponent implements AfterContentInit {
private data_provider: dataProvider, private data_provider: dataProvider,
) {} ) {}
show_history(item:any) { applyFilterGlobalLeases($event: any, stringVal: string) {
this.dtLeases.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
applyFilterGlobalHistory($event: any, stringVal: string) {
this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
show_history(item: any) {
var _self = this; var _self = this;
this.data_provider.getDhcpHistory(item).then((res) => { this.data_provider.getDhcpHistory(item).then((res) => {
_self.current_dhcp = item; _self.current_dhcp = item;

View file

@ -23,7 +23,9 @@ import { WidgetsModule } from "../../widgets/widgets.module";
// import { WidgetsRoutingModule } from './widgets-routing.module'; // import { WidgetsRoutingModule } from './widgets-routing.module';
import { DhcpInfoComponent } from './dhcp-info.component'; import { DhcpInfoComponent } from './dhcp-info.component';
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -42,7 +44,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
WidgetsModule, WidgetsModule,
NavbarModule, NavbarModule,
ModalModule, ModalModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
TableModule, TableModule,
UtilitiesModule, UtilitiesModule,
BadgeModule, BadgeModule,

View file

@ -9,7 +9,8 @@
{{ping['successful_pings']}}</c-badge></h6> {{ping['successful_pings']}}</c-badge></h6>
<h6 style="display: inline-block;margin-left: 0.5rem;"> | Failed pings : <c-badge color="danger" <h6 style="display: inline-block;margin-left: 0.5rem;"> | Failed pings : <c-badge color="danger"
style="font-size: 90%;"> {{ping['failed_pings']}}</c-badge></h6> style="font-size: 90%;"> {{ping['failed_pings']}}</c-badge></h6>
<h6 style="display: inline-block;margin-left: 0.5rem;">| Avrage : <c-badge color="dark" style="font-size: 90%;"> <h6 style="display: inline-block;margin-left: 0.5rem;">| Average : <c-badge color="dark"
style="font-size: 90%;">
{{ping['average_ping_time']}} ms</c-badge></h6> {{ping['average_ping_time']}} ms</c-badge></h6>
<table cTable small> <table cTable small>
<thead> <thead>

View file

@ -4,7 +4,7 @@
<c-card-header> <c-card-header>
<c-row> <c-row>
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;"> <c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
<h5>Device LOGS <h5>Device Logs
<a style="cursor: pointer;" (click)="reinitgrid('none','none')"><i <a style="cursor: pointer;" (click)="reinitgrid('none','none')"><i
*ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate" *ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate"
style="color: #74C0FC;"></i> style="color: #74C0FC;"></i>
@ -14,8 +14,10 @@
</h5> </h5>
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID <c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
{{devid}}</c-badge> {{devid}}</c-badge>
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']"> <c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time. *ngIf="!filters['start_time'] && !filters['end_time']">
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
default. Use filters to modify the date and time.
</c-alert> </c-alert>
</c-col> </c-col>
<c-col xs [lg]="1"> <c-col xs [lg]="1">
@ -84,74 +86,207 @@
</div> </div>
</c-row> </c-row>
<gui-grid wid [rowDetail]="rowDetail" [horizontalGrid]="true" [rowHeight]="52" [source]="source" <div class="mb-3 d-flex justify-content-end">
[columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left table-search-input">
[autoResizeWidth]="true"> <i class="pi pi-search"></i>
<gui-grid-column header="#No" type="NUMBER" field="index" width="1" align="CENTER"> <input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
<ng-template let-value="item.index" let-item="item" let-index="index"> class="form-control-sm" />
{{ value }} </span>
</ng-template> </div>
</gui-grid-column>
<gui-grid-column header="level" width='90' wid field="level"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<ng-template let-value="item.level" let-item="item" let-index="index"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
<c-badge style="cursor: pointer; font-weight: normal" color="danger" *ngIf="value == 'Critical'">{{ value [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
}}</c-badge> [globalFilterFields]="['detail', 'comment', 'src', 'name', 'devip', 'level']" selectionMode="single"
<c-badge style="cursor: pointer; font-weight: normal" color="warning" *ngIf="value == 'Error'">{{ value (onRowSelect)="showLogDetails($event.data)">
}}</c-badge>
<c-badge style="cursor: pointer; font-weight: normal" color="warning" *ngIf="value == 'Warning'">{{ value
}}</c-badge>
<c-badge style="cursor: pointer; font-weight: normal; min-width: 60px;" color="info"
*ngIf="value == 'info'">{{ value }}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Event" width='200' field="detail">
<ng-template let-value="item.detail" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Detail" field="comment">
<ng-template let-value="item.comment" let-item="item" let-index="index">
<div class="gui-dev-info"> <ng-template pTemplate="header">
{{ value }} <tr>
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
<div class="justify-between">
<span>#No</span>
<p-sortIcon field="index"></p-sortIcon>
</div>
</th>
<th pSortableColumn="level" style="width: 100px" pResizableColumn>
<div class="justify-between">
<span>Level</span>
<span>
<p-sortIcon field="level"></p-sortIcon>
<p-columnFilter type="text" field="level" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="detail" pResizableColumn>
<div class="justify-between">
<span>Event</span>
<span>
<p-sortIcon field="detail"></p-sortIcon>
<p-columnFilter type="text" field="detail" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="comment" pResizableColumn>
<div class="justify-between">
<span>Detail</span>
<span>
<p-sortIcon field="comment"></p-sortIcon>
<p-columnFilter type="text" field="comment" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="src" style="width: 100px" pResizableColumn>
<div class="justify-between">
<span>Source</span>
<span>
<p-sortIcon field="src"></p-sortIcon>
<p-columnFilter type="text" field="src" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="status" style="width: 100px" pResizableColumn>
<div class="justify-between">
<span>Status</span>
<span>
<p-sortIcon field="status"></p-sortIcon>
<p-columnFilter type="text" field="status" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="eventtime" style="width: 200px" pResizableColumn>
<div class="justify-between">
<span>Time</span>
<span>
<p-sortIcon field="eventtime"></p-sortIcon>
<p-columnFilter type="text" field="eventtime" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Device</span>
<span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</span>
</div>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr [pSelectableRow]="item" style="cursor: pointer;">
<td class="text-center">{{item.index}}</td>
<td>
<c-badge
[color]="item.level == 'Critical' ? 'danger' : (item.level == 'Error' || item.level == 'Warning' ? 'warning' : 'info')"
style="font-weight: normal; min-width: 65px;">
{{item.level}}
</c-badge>
</td>
<td>{{item.detail}}</td>
<td>
<div class="text-truncate" style="max-width: 250px;" [pTooltip]="item.comment" tooltipPosition="top">
{{item.comment}}
</div>
</td>
<td>{{item.src}}</td>
<td class="text-center">
<c-badge [color]="item.status == true ? 'success' : 'danger'" style="font-weight: normal">
{{item.status == true ? 'Fixed' : 'Not Fixed'}}
</c-badge>
</td>
<td class="small">
<div *ngIf="item.fixtime" class="d-flex flex-column gap-1">
<div class="d-flex align-items-center gap-1">
<c-badge color="danger" style="font-size: 0.6rem; padding: 2px 4px;">EVT</c-badge>
<span>{{item.eventtime}}</span>
</div>
<div class="d-flex align-items-center gap-1">
<c-badge color="success" style="font-size: 0.6rem; padding: 2px 4px;">FIX</c-badge>
<span>{{item.fixtime}}</span>
</div>
</div>
<div *ngIf="!item.fixtime">{{item.eventtime}}</div>
</td>
<td>
<div class="fw-bold">{{item.name}}</div>
<div class="small text-muted">{{item.devip}}</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="8" class="text-center p-4">No device logs found.</td>
</tr>
</ng-template>
</p-table>
<!-- Details Drawer -->
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '500px'}" header="Device Log Details">
<div *ngIf="selectedLog" class="p-0">
<div [style.background-color]="getSeverityColor(selectedLog.level)" class="p-4 text-white shadow-sm mb-4">
<div class="d-flex align-items-center justify-content-between">
<div>
<h3 class="m-0"><i class="pi pi-exclamation-triangle me-2"></i>{{selectedLog.level}} Alert</h3>
<div class="opacity-75">{{selectedLog.eventtype || 'System Event'}}</div>
</div>
<i class="pi pi-bell" style="font-size: 2.5rem; opacity: 0.3;"></i>
</div> </div>
</ng-template> </div>
</gui-grid-column>
<gui-grid-column header="Source" width='90' field="src"> <div class="px-4 pb-4">
<ng-template let-value="item.src" let-item="item" let-index="index"> <h5 class="border-bottom pb-2 mb-3"><i class="pi pi-desktop me-2 text-primary"></i>Device Info</h5>
{{ value }} <div class="row mb-2">
</ng-template> <div class="col-5 fw-bold text-muted small text-uppercase">Device Name:</div>
</gui-grid-column> <div class="col-7 fw-bold">{{selectedLog.name}}</div>
<gui-grid-column header="status" width='100' field="status" align="CENTER">
<ng-template let-value="item.status" let-item="item" let-index="index">
<c-badge style=" cursor: pointer; font-weight: normal" color="success"
*ngIf="value == true">Fixed</c-badge>
<c-badge style="cursor: pointer; font-weight: normal" color="danger" *ngIf="value != true">Not
Fixed</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="eventtime" width='220' field="eventtime">
<ng-template let-value="item.eventtime" let-item="item" let-index="index">
<div *ngIf="item.fixtime" class="fixed_time"><span><c-badge *ngIf="item.status==true" color="danger"
style="font-size: 0.65em!important;padding: 3px 5px;" size="sm" shape="rounded-pill">Event</c-badge>
{{value}}</span><span>
<c-badge *ngIf="item.status==true" color="success"
style="font-size: 0.65em!important;padding: 3px 5px;" size="sm" shape="rounded-pill">Fixed</c-badge>
{{item.fixtime}}</span>
</div> </div>
<div *ngIf="!item.fixtime"> {{ value }}</div> <div class="row mb-2">
</ng-template> <div class="col-5 fw-bold text-muted small text-uppercase">IP Address:</div>
</gui-grid-column> <div class="col-7 font-monospace">{{selectedLog.devip}}</div>
<gui-grid-column header="Device" width='200' field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<div class="gui-dev-info">
<span class="gui-dev-info-name">{{ value }}</span>
<span class="gui-dev-info-ip">{{ item.devip }}</span>
</div> </div>
</ng-template> <div class="row mb-2">
</gui-grid-column> <div class="col-5 fw-bold text-muted small text-uppercase">MAC Address:</div>
</gui-grid> <div class="col-7 small font-monospace">{{selectedLog.mac}}</div>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-info-circle me-2 text-primary"></i>Event Details
</h5>
<div class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">Event:</div>
<div class="col-7">{{selectedLog.detail}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">Source:</div>
<div class="col-7">{{selectedLog.src}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">Event Time:</div>
<div class="col-7 small">{{selectedLog.eventtime}}</div>
</div>
<div *ngIf="selectedLog.fixtime" class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">Fixed Time:</div>
<div class="col-7 small text-success fw-bold">{{selectedLog.fixtime}}</div>
</div>
<div class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">Status:</div>
<div class="col-7">
<c-badge [color]="selectedLog.status == true ? 'success' : 'danger'" style="padding: 5px 10px;">
{{selectedLog.status == true ? 'Fixed' : 'Not Fixed'}}
</c-badge>
</div>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-comment me-2 text-primary"></i>Detail Message
</h5>
<div class="bg-light p-3 rounded border italic text-muted text-break"
style="font-size: 0.9rem; line-height: 1.4;">
{{selectedLog.comment || 'No additional details available for this event.'}}
</div>
</div>
</div>
</p-drawer>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>

View file

@ -1,19 +1,9 @@
import { Component, OnInit, ViewEncapsulation,Input } from "@angular/core"; import { Component, OnInit, ViewChild, ViewEncapsulation, Input } from "@angular/core";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiRowDetail,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { takeUntil } from "rxjs/operators"; import { takeUntil } from "rxjs/operators";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
@ -27,10 +17,14 @@ import { Subject } from "rxjs";
}) })
export class DevLogsComponent implements OnInit { export class DevLogsComponent implements OnInit {
@Input() component_devid: any=false; @Input() component_devid: any=false;
public uid: number; public uid!: number;
public uname: string; public uname!: string;
public tz: string = "UTC" public tz: string = "UTC"
public filterText: string; public filterText!: string;
public detailsVisible: boolean = false;
public selectedLog: any = null;
@ViewChild('dt') table!: Table;
public filters: any = { public filters: any = {
start_time: false, start_time: false,
end_time: false, end_time: false,
@ -73,108 +67,28 @@ export class DevLogsComponent implements OnInit {
} }
} }
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public Selectedrows: any; public selected_rows: any[] = [];
public Selectedrows: any[] = [];
public devid: number = 0; public devid: number = 0;
public sorting = {
enabled: true,
multiSorting: true,
};
public bankMultiFilterCtrl: FormControl = new FormControl<string>(""); public bankMultiFilterCtrl: FormControl = new FormControl<string>("");
protected _onDestroy = new Subject<void>(); protected _onDestroy = new Subject<void>();
public campaignOnestart: any; getSeverityColor(level: string): string {
public campaignOneend: any; if (level === "Critical") return "#e55353";
rowDetail: GuiRowDetail = { if (level === "Warning") return "#f9b115";
enabled: true, return "#3399ff";
template: (item) => { }
return `
<div class='log-detail' style="width: 355px;color:#fff;background-color:${(() => {
if (item.level == "Critical") return "#e55353";
else if (item.level == "Warning") return "#f9b115";
else item.level == "Info";
return "#3399ff";
})()}">
<h1>Device :</h1>
<table>
<tr>
<td>Device Name</td>
<td>${item.name}</td>
</tr>
<tr>
<td>Device IP</td>
<td>${item.devip}</td>
</tr>
<tr>
<td>Device MAC</td>
<td>${item.mac}</td>
</tr>
</table>
<h1 style="margin-top: 10px;">Alert Detail :
</h1> showLogDetails(log: any) {
<table> this.selectedLog = log;
<tr> this.detailsVisible = true;
<td>Event</td> }
<td>${item.detail}</td>
</tr>
<tr>
<td>Event Status</td>
<td><span (click)="logger(${item})" style="display:inline-block;background-color:${
item.status ? "green" : "#db4848"
} ;padding: 4px 10px;border-radius: 5px;line-height: 10px;color: rgba(255, 255, 255, 0.87);">${
item.status ? "Fixed" : "Not Fixed"
}</span></td>
</tr>
<tr>
<td>Event Category</td>
<td>${item.eventtype}</td>
</tr>
<tr>
<td>Exec time</td>
<td>${item.eventtime}</td>
</tr>
<tr>
<td>Detail</td>
<td>${item.comment}</td>
</tr>
<tr>
<td>Source</td>
<td>${item.src}</td>
</tr>
</table>
</div>`;
},
};
public paging: GuiPaging = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
page: 1, }
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
ngOnInit(): void { ngOnInit(): void {
var _self = this; var _self = this;
if (this.component_devid) { if (this.component_devid) {

View file

@ -15,7 +15,10 @@ import {
import { NgxMatSelectSearchModule } from "ngx-mat-select-search"; import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
import { DevLogsRoutingModule } from "./devlogs-routing.module"; import { DevLogsRoutingModule } from "./devlogs-routing.module";
import { DevLogsComponent } from "./devlogs.component"; import { DevLogsComponent } from "./devlogs.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule } from 'primeng/table';
import { DrawerModule } from 'primeng/drawer';
import { InputTextModule } from 'primeng/inputtext';
import { TooltipModule } from 'primeng/tooltip';
import { MatDatepickerModule } from "@angular/material/datepicker"; import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatInputModule } from "@angular/material/input"; import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field"; import { MatFormFieldModule } from "@angular/material/form-field";
@ -34,7 +37,10 @@ import { MatSelectModule } from "@angular/material/select";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, TableModule,
DrawerModule,
InputTextModule,
TooltipModule,
CollapseModule, CollapseModule,
BadgeModule, BadgeModule,
MatInputModule, MatInputModule,

View file

@ -8,12 +8,12 @@
</c-col> </c-col>
<c-col xs [lg]="9"> <c-col xs [lg]="9">
<h6 style="text-align: right;"> <h6 style="text-align: right;">
<button cButton color="success" (click)="openAddDeviceModal()" class="mx-1" <button cButton color="success" (click)="openAddDeviceModal()" class="mx-1" size="sm"
size="sm" style="color: #fff;"><i class="fa-solid fa-plus"></i> Bulk Add </button> style="color: #fff;"><i class="fa-solid fa-plus"></i> Bulk Add </button>
<button cButton color="dark" (click)="scanwizard(1,'')" [cModalToggle]="ScannerModal.id" class="mx-1" <button cButton color="dark" (click)="scanwizard(1,'')" [cModalToggle]="ScannerModal.id" class="mx-1"
size="sm" style="color: #fff;"><i class="fa-solid fa-magnifying-glass"></i> Scan</button> size="sm" style="color: #fff;"><i class="fa-solid fa-magnifying-glass"></i> Scan</button>
<button cButton color="primary" (click)="show_exec()" class="mx-1" <button cButton color="primary" (click)="show_exec()" class="mx-1" size="sm" style="color: #fff;"><i
size="sm" style="color: #fff;"><i class="fa-solid fa-history"></i> History</button> class="fa-solid fa-history"></i> History</button>
</h6> </h6>
</c-col> </c-col>
</c-row> </c-row>
@ -21,9 +21,11 @@
<c-card-body> <c-card-body>
<c-row> <c-row>
<c-col [lg]="9"> <c-col [lg]="9">
<button cButton color="danger" class="mx-1" size="sm" style="color: #fff;" (click)="filterUpdatable()">{{updates.length}} Updatable <button cButton color="danger" class="mx-1" size="sm" style="color: #fff;"
(click)="filterUpdatable()">{{updates.length}} Updatable
</button> </button>
<button cButton color="warning" class="mx-1" size="sm" style="color: #fff;" (click)="filterUpgradable()">{{upgrades.length}} <button cButton color="warning" class="mx-1" size="sm" style="color: #fff;"
(click)="filterUpgradable()">{{upgrades.length}}
Upgradable</button> Upgradable</button>
<button cButton color="secondary" class="mx-1" size="sm" (click)="clearFilter()">Clear Filter</button> <button cButton color="secondary" class="mx-1" size="sm" (click)="clearFilter()">Clear Filter</button>
</c-col> </c-col>
@ -38,116 +40,162 @@
</c-input-group> </c-input-group>
</c-col> </c-col>
</c-row> </c-row>
<gui-grid #grid [rowClass]="rowClass" [source]="source" [searching]="searching" [paging]="paging" <div class="mb-1 d-flex justify-content-end">
[columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection" <span class="p-input-icon-left table-search-input">
(selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true> <i class="pi pi-search"></i>
<gui-grid-column header="Name" field="name"> <input type="text" pInputText placeholder="Search devices..."
<ng-template let-value="item.name" let-item="item" let-index="index"> (input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
<button cButton size="sm" variant="ghost" color="primary" (click)="webAccess(item)" </span>
style="padding: 2px 4px; margin-right: 5px; border: none;" cTooltip="Web Access"> </div>
<i class="fas fa-globe" style="font-size: 12px;"></i>
</button> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" /> [(selection)]="selected_rows" (selectionChange)="onSelectionChange($event)"
<i *ngIf="item.status=='updated'" cTooltip="Tooltip text" [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
style="color: green; margin-right: 3px;font-size: .7em;" class="fa-solid fa-check"></i> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
<i *ngIf="item.status=='failed'" cTooltip="Update failed" [globalFilterFields]="['name','ip','mac','arch']">
style="color: red; margin-right: 3px;font-size: .7em;" class="fa-solid fa-x"></i>
{{value}} <ng-template pTemplate="header">
</ng-template> <tr>
</gui-grid-column> <th style="width: 3rem" pResizableColumn>
<gui-grid-column header="CPU Type" field="arch"> <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
<ng-template let-value="item.arch" let-item="item" let-index="index"> </th>
{{value}} <th pSortableColumn="name" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>Name</span>
<gui-grid-column header="Firmware" field="current_firmware"> <p-sortIcon field="name"></p-sortIcon>
<ng-template let-value="item.current_firmware" let-item="item" let-index="index"> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
<div>{{value}}</div>
<i *ngIf="item.update_availble" cTooltip="Firmware Update availble"
class="fa-solid fa-up-long text-primary mx-1"></i>
<i *ngIf="item.upgrade_availble" cTooltip="Device Firmware not Upgraded"
class="fa-solid fa-microchip text-danger mx-1"></i>
</ng-template>
</gui-grid-column>
<gui-grid-column header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="License" field="license" [enabled]="false">
<ng-template let-value="item.license" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Interface" field="interface" [enabled]="false">
<ng-template let-value="item.interface" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Created" field="created" [enabled]="false">
<ng-template let-value="item.created" let-item="item.id" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Uptime" field="uptime">
<ng-template let-value="item.uptime" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Created" field="created" [enabled]="false">
<ng-template let-value="item.created" let-item="item.id" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" [cellEditing]="false" [sorting]="false" header="Action">
<ng-template let-value="value" let-item="item">
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary" (click)="show_detail(item)"
style="border: none;padding: 4px 7px;"><i class="fa-regular fa-eye"></i><small> Details</small>
</button>
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
[matMenuTriggerFor]="menu" cButton>
<i class="fa-solid fa-bars"></i>
</button>
<mat-menu #menu="matMenu">
<div cListGroup>
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
<button size="sm" (click)="single_device_action(item,'edit')" style="padding: 4px 7px;"
cListGroupItem><i class="fa-solid fa-pencil"></i><small>
Edit Device</small></button>
<button size="sm" (click)="single_device_action(item,'firmware')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-magnifying-glass"></i><small>
Check Firmware</small></button>
<button size="sm" (click)="single_device_action(item,'update')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-upload"></i><small>
Update Firmware</small></button>
<button size="sm" (click)="single_device_action(item,'upgrade')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-microchip"></i><small>
Upgrade Firmware</small></button>
<button size="sm" (click)="single_device_action(item,'devlogs')" style="padding: 4px 7px;"
cListGroupItem><i class="fa-regular fa-rectangle-list"></i><small>
Device Logs</small></button>
<button size="sm" (click)="single_device_action(item,'logauth')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-regular fa-clock"></i><small>
Show Auth Logs</small></button>
<button size="sm" (click)="single_device_action(item,'logacc')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-table-list"></i><small>
Show Acc Logs</small></button>
<button size="sm" (click)="single_device_action(item,'backup')" style="padding: 4px 7px;"
cListGroupItem><i class="text-success fa-solid fa-database"></i><small>
Show Backups</small></button>
<button size="sm" (click)="single_device_action(item,'delete')" style="padding: 4px 7px;"
cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
Delete Device</small></button>
</div> </div>
</mat-menu> </th>
</ng-template> <th pSortableColumn="arch" pResizableColumn>
</gui-grid-column> <div class="justify-between">
</gui-grid> <span>CPU Type</span>
<span>
<p-sortIcon field="arch"></p-sortIcon>
<p-columnFilter type="text" field="arch" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="current_firmware" pResizableColumn>
<div class="justify-between">
<span>Current Firmware</span>
<span>
<p-sortIcon field="current_firmware"></p-sortIcon>
<p-columnFilter type="text" field="current_firmware" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="ip" pResizableColumn>
<div class="justify-between">
<span>IP Address</span>
<span>
<p-sortIcon field="ip"></p-sortIcon>
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="mac" pResizableColumn>
<div class="justify-between">
<span>MAC Address</span>
<span>
<p-sortIcon field="mac"></p-sortIcon>
<p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="uptime" pResizableColumn>
<div class="justify-between">
<span>Uptime</span>
<span>
<p-sortIcon field="uptime"></p-sortIcon>
<p-columnFilter type="text" field="uptime" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr [ngClass]="item.status === 'updating' ? 'row-highlighted' : ''">
<td>
<p-tableCheckbox [value]="item"></p-tableCheckbox>
</td>
<td>
<button cButton size="sm" variant="ghost" color="primary" (click)="webAccess(item)"
style="padding: 2px 4px; margin-right: 5px; border: none;" cTooltip="Web Access">
<i class="fas fa-globe" style="font-size: 12px;"></i>
</button>
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
<i *ngIf="item.status=='updated'" cTooltip="Updated"
style="color: green; margin-right: 3px;font-size: .7em;" class="fa-solid fa-check"></i>
<i *ngIf="item.status=='failed'" cTooltip="Update failed"
style="color: red; margin-right: 3px;font-size: .7em;" class="fa-solid fa-x"></i>
{{item.name}}
</td>
<td>{{item.arch}}</td>
<td>
<div style="display:inline-block">{{item.current_firmware}}</div>
<i *ngIf="item.update_availble" cTooltip="Firmware Update available"
class="fa-solid fa-up-long text-primary mx-1"></i>
<i *ngIf="item.upgrade_availble" cTooltip="Device Firmware not Upgraded"
class="fa-solid fa-microchip text-danger mx-1"></i>
</td>
<td>{{item.ip}}</td>
<td>{{item.mac}}</td>
<td>{{item.uptime}}</td>
<td class="text-center">
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary"
(click)="show_detail(item)" style="border: none;padding: 4px 7px;"><i
class="fa-regular fa-eye"></i><small> Details</small>
</button>
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
[matMenuTriggerFor]="menu" cButton>
<i class="fa-solid fa-bars"></i>
</button>
<mat-menu #menu="matMenu">
<div cListGroup>
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
<button size="sm" (click)="single_device_action(item,'edit')" style="padding: 4px 7px;"
cListGroupItem><i class="fa-solid fa-pencil"></i><small>
Edit Device</small></button>
<button size="sm" (click)="single_device_action(item,'firmware')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-magnifying-glass"></i><small>
Check Firmware</small></button>
<button size="sm" (click)="single_device_action(item,'update')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-upload"></i><small>
Update Firmware</small></button>
<button size="sm" (click)="single_device_action(item,'upgrade')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-microchip"></i><small>
Upgrade Firmware</small></button>
<button size="sm" (click)="single_device_action(item,'devlogs')" style="padding: 4px 7px;"
cListGroupItem><i class="fa-regular fa-rectangle-list"></i><small>
Device Logs</small></button>
<button size="sm" (click)="single_device_action(item,'logauth')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-regular fa-clock"></i><small>
Show Auth Logs</small></button>
<button size="sm" (click)="single_device_action(item,'logacc')" style="padding: 4px 7px;"
cListGroupItem><i class="text-primary fa-solid fa-table-list"></i><small>
Show Acc Logs</small></button>
<button size="sm" (click)="single_device_action(item,'backup')" style="padding: 4px 7px;"
cListGroupItem><i class="text-success fa-solid fa-database"></i><small>
Show Backups</small></button>
<button size="sm" (click)="single_device_action(item,'delete')" style="padding: 4px 7px;"
cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
Delete Device</small></button>
</div>
</mat-menu>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="8">No devices found.</td>
</tr>
</ng-template>
</p-table>
<c-navbar *ngIf="rows.length!= 0" class="bg-light" colorScheme="light" expand="lg"> <c-navbar *ngIf="rows.length!= 0" class="bg-light" colorScheme="light" expand="lg">
<c-container [fluid]="true"> <c-container [fluid]="true">
<a cNavbarBrand href="javascript:;"> <a cNavbarBrand href="javascript:;">
@ -292,54 +340,105 @@
</select> </select>
</c-input-group> </c-input-group>
</div> </div>
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching" <div class="mb-3 d-flex justify-content-end">
[source]="filteredExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [paging]="paging"> <span class="p-input-icon-left">
<gui-grid-column header="Type" field="task_type" [width]="100"> <i class="pi pi-search"></i>
<ng-template let-value="item['task_type']" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search history..."
<i *ngIf="item.task_type === 'ip-scan'" class="fas fa-search" style="margin-right: 3px; color: #0d6efd;"></i> (input)="dtTask.filterGlobal($any($event.target).value, 'contains')" class="form-control-sm" />
<i *ngIf="item.task_type === 'bulk-add'" class="fas fa-plus-circle" style="margin-right: 3px; color: #198754;"></i> </span>
<span style="font-size: 11px;">{{getTaskTypeLabel(value)}}</span> </div>
</ng-template>
</gui-grid-column> <p-table #dtTask [value]="filteredExecutedData" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<gui-grid-column header="Time" field="time" [width]="220"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
<ng-template let-value="value" let-item="item" let-index="index"> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
<div style="font-size: 10px; line-height: 1.3;"> [globalFilterFields]="['task_id', 'username', 'start_ip', 'end_ip']">
<div><strong>Start:</strong> {{item.started}}</div>
<div><strong>End:</strong> {{item.ended}}</div> <ng-template pTemplate="header">
</div> <tr>
</ng-template> <th pSortableColumn="index" style="width: 5rem" pResizableColumn>
</gui-grid-column> <div class="justify-between">
<gui-grid-column header="Details" field="details"> <span>#No</span>
<ng-template let-value="value" let-item="item" let-index="index"> <p-sortIcon field="index"></p-sortIcon>
<div *ngIf="item.task_type === 'ip-scan'" style="font-size: 12px; line-height: 1.3;"> </div>
<div>{{item.start_ip}} - {{item.end_ip}}</div> </th>
<div>User: {{item.username}}</div> <th pSortableColumn="name" pResizableColumn>
</div> <div class="justify-between">
<div *ngIf="item.task_type === 'bulk-add'" style="font-size: 12px; line-height: 1.3;"> <span>Name</span>
<div>{{item.device_count}} devices</div> <span>
<div style="word-break: break-all;">{{item.task_id}}</div> <p-sortIcon field="name"></p-sortIcon>
</div> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="Results" field="results" align="center" [width]="70"> </th>
<ng-template let-value="value" let-item="item" let-index="index"> <th pSortableColumn="createdC" pResizableColumn>
<div style="font-size: 11px;"> <div class="justify-between">
<span style="color: green;">✓ {{item.success_count}}</span> <span>Execution Time</span>
<span *ngIf="item.failed_count > 0" style="color: red; margin-left: 5px;">✗ {{item.failed_count}}</span> <span>
</div> <p-sortIcon field="createdC"></p-sortIcon>
</ng-template> <p-columnFilter type="text" field="createdC" display="menu" class="ms-auto" />
</gui-grid-column> </span>
<gui-grid-column header="Actions" field="actions" align="center" [width]="160"> </div>
<ng-template let-value="value" let-item="item" let-index="index"> </th>
<button (click)="showTaskDetails(item)" color="info" size="sm" cButton class="me-1"> <th pSortableColumn="username" pResizableColumn>
<i class="fas fa-eye"></i> Details <div class="justify-between">
</button> <span>User</span>
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton> <span>
<i class="fas fa-download"></i> CSV <p-sortIcon field="username"></p-sortIcon>
</button> <p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</ng-template> </span>
</gui-grid-column> </div>
</gui-grid> </th>
<th style="width: 100px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>
<div class="d-flex align-items-center gap-1">
<i *ngIf="item.task_type === 'ip-scan'" class="fas fa-search text-primary"></i>
<i *ngIf="item.task_type === 'bulk-add'" class="fas fa-plus-circle text-success"></i>
<span class="small">{{getTaskTypeLabel(item.task_type)}}</span>
</div>
</td>
<td class="small">
<div><strong>S:</strong> {{item.started}}</div>
<div><strong>E:</strong> {{item.ended}}</div>
</td>
<td>
<div *ngIf="item.task_type === 'ip-scan'" class="small">
<div>{{item.start_ip}} - {{item.end_ip}}</div>
<div class="text-muted">User: {{item.username}}</div>
</div>
<div *ngIf="item.task_type === 'bulk-add'" class="small">
<div>{{item.device_count}} devices</div>
<div class="text-muted text-truncate" style="max-width: 150px;">{{item.task_id}}</div>
</div>
</td>
<td class="text-center">
<div class="d-flex flex-column align-items-center">
<span class="text-success small">✓ {{item.success_count}}</span>
<span *ngIf="item.failed_count > 0" class="text-danger small">✗ {{item.failed_count}}</span>
</div>
</td>
<td class="text-center">
<button (click)="showTaskDetails(item)" color="info" size="sm" cButton class="me-1"
style="padding: 2px 6px;">
<i class="fas fa-eye"></i> Details
</button>
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton style="padding: 2px 6px;">
<i class="fas fa-download"></i> CSV
</button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="5" class="text-center p-4">No task history found.</td>
</tr>
</ng-template>
</p-table>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
<button (click)="ExecutedDataModalVisible=!ExecutedDataModalVisible" cButton color="secondary"> <button (click)="ExecutedDataModalVisible=!ExecutedDataModalVisible" cButton color="secondary">
@ -453,7 +552,8 @@
<c-modal-body> <c-modal-body>
<div *ngIf="addDeviceStep === 1"> <div *ngIf="addDeviceStep === 1">
<h6>Upload CSV File</h6> <h6>Upload CSV File</h6>
<p>Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port</p> <p>Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port
</p>
<input type="file" accept=".csv" (change)="onFileSelected($event)" class="form-control mb-3"> <input type="file" accept=".csv" (change)="onFileSelected($event)" class="form-control mb-3">
<div *ngIf="csvPreview.length > 0"> <div *ngIf="csvPreview.length > 0">
<h6>Preview (First 3 rows):</h6> <h6>Preview (First 3 rows):</h6>
@ -522,7 +622,8 @@
</div> </div>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
<button *ngIf="addDeviceStep === 1" cButton color="primary" (click)="uploadDevices()" [disabled]="!isValidMapping()"> <button *ngIf="addDeviceStep === 1" cButton color="primary" (click)="uploadDevices()"
[disabled]="!isValidMapping()">
Add Devices Add Devices
</button> </button>
<button *ngIf="addDeviceStep === 3" cButton color="success" (click)="closeAddDeviceModal()"> <button *ngIf="addDeviceStep === 3" cButton color="success" (click)="closeAddDeviceModal()">
@ -558,12 +659,13 @@
<!-- Task Details Modal --> <!-- Task Details Modal -->
<c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal" <c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal"
style="z-index: 1060; backdrop-filter: blur(2px);"> style="z-index: 1060; backdrop-filter: blur(2px);">
<c-modal-header style="background: rgba(255,255,255,0.95); backdrop-filter: blur(10px);"> <c-modal-header style="background: rgba(255,255,255,0.95); backdrop-filter: blur(10px);">
<h5 cModalTitle>{{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results</h5> <h5 cModalTitle>{{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results</h5>
<button (click)="closeDetailsModal()" cButtonClose></button> <button (click)="closeDetailsModal()" cButtonClose></button>
</c-modal-header> </c-modal-header>
<c-modal-body *ngIf="selectedTaskDetails" style="background: rgba(255,255,255,0.98); backdrop-filter: blur(10px); max-height: 70vh; overflow-y: auto;"> <c-modal-body *ngIf="selectedTaskDetails"
style="background: rgba(255,255,255,0.98); backdrop-filter: blur(10px); max-height: 70vh; overflow-y: auto;">
<div class="mb-3"> <div class="mb-3">
<c-row> <c-row>
<c-col md="6"> <c-col md="6">
@ -588,7 +690,7 @@
<h6 class="mb-0">Detailed Results:</h6> <h6 class="mb-0">Detailed Results:</h6>
<div class="col-md-4"> <div class="col-md-4">
<input type="text" class="form-control form-control-sm" placeholder="Search IP or error..." <input type="text" class="form-control form-control-sm" placeholder="Search IP or error..."
[(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()"> [(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()">
</div> </div>
</div> </div>
<table class="table table-striped table-hover table-responsive"> <table class="table table-striped table-hover table-responsive">
@ -620,19 +722,20 @@
<nav *ngIf="getTotalDetailsPages() > 1" class="mt-3"> <nav *ngIf="getTotalDetailsPages() > 1" class="mt-3">
<ul class="pagination pagination-sm justify-content-center"> <ul class="pagination pagination-sm justify-content-center">
<li class="page-item" [class.disabled]="detailsCurrentPage === 1"> <li class="page-item" [class.disabled]="detailsCurrentPage === 1">
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage - 1)" [disabled]="detailsCurrentPage === 1"> <button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage - 1)"
[disabled]="detailsCurrentPage === 1">
Previous Previous
</button> </button>
</li> </li>
<li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index" <li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index"
[class.active]="detailsCurrentPage === i + 1"> [class.active]="detailsCurrentPage === i + 1">
<button class="page-link" (click)="onDetailsPageChange(i + 1)"> <button class="page-link" (click)="onDetailsPageChange(i + 1)">
{{i + 1}} {{i + 1}}
</button> </button>
</li> </li>
<li class="page-item" [class.disabled]="detailsCurrentPage === getTotalDetailsPages()"> <li class="page-item" [class.disabled]="detailsCurrentPage === getTotalDetailsPages()">
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)" <button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)"
[disabled]="detailsCurrentPage === getTotalDetailsPages()"> [disabled]="detailsCurrentPage === getTotalDetailsPages()">
Next Next
</button> </button>
</li> </li>

View file

@ -10,21 +10,9 @@ import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import {
GuiGridComponent, ToasterComponent
GuiGridApi, } from "@coreui/angular";
GuiRowClass, import { Table } from 'primeng/table';
GuiSearching,
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component"; import { AppToastComponent } from "../toast-simple/toast.component";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
@ -34,9 +22,9 @@ import { formatInTimeZone } from "date-fns-tz";
templateUrl: "devices.component.html", templateUrl: "devices.component.html",
}) })
export class DevicesComponent implements OnInit, OnDestroy { export class DevicesComponent implements OnInit, OnDestroy {
public uid: number; public uid!: number;
public uname: string; public uname!: string;
public tz: string; public tz!: string;
public ispro:boolean=false; public ispro:boolean=false;
constructor( constructor(
@ -69,14 +57,11 @@ export class DevicesComponent implements OnInit, OnDestroy {
return value !== undefined && value !== null && value !== ""; return value !== undefined && value !== null && value !== "";
} }
} }
@ViewChild("grid", { static: true }) gridComponent: GuiGridComponent; @ViewChild("dt") table!: Table;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public originalSource: Array<any> = []; public originalSource: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = [];
public Selectedrows: any;
public upgrades: any = []; public upgrades: any = [];
public updates: any = []; public updates: any = [];
public scanwizard_step: number = 1; public scanwizard_step: number = 1;
@ -118,6 +103,10 @@ export class DevicesComponent implements OnInit, OnDestroy {
public currentTaskId: string = ''; public currentTaskId: string = '';
public statusCheckTimer: any; public statusCheckTimer: any;
public selected_rows: any[] = []; // Used by p-table selection
public Selectedrows: any[] = []; // Legacy ID array used by actions
public rows: any = []; // For legacy internal use
toasterForm = { toasterForm = {
autohide: true, autohide: true,
delay: 3000, delay: 3000,
@ -125,7 +114,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
fade: true, fade: true,
closeButton: true, closeButton: true,
}; };
rowClass: GuiRowClass = { rowClass: any = {
class: "row-highlighted", class: "row-highlighted",
}; };
public sorting = { public sorting = {
@ -134,37 +123,35 @@ export class DevicesComponent implements OnInit, OnDestroy {
}; };
public ip_scanner: any; public ip_scanner: any;
searching: GuiSearching = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
placeholder: "Search Devices", }
};
public paging: GuiPaging = { public paging: any = {
enabled: true, enabled: true,
page: 1, page: 1,
pageSize: 10, pageSize: 10,
pageSizes: [5, 10, 25, 50], pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
}; };
public columnMenu: GuiColumnMenu = { public columnMenu: any = {
enabled: true, enabled: true,
sort: true, sort: true,
columnsManager: true, columnsManager: true,
}; };
public infoPanel: GuiInfoPanel = { public infoPanel: any = {
enabled: true, enabled: true,
infoDialog: false, infoDialog: false,
columnsManager: true, columnsManager: true,
schemaManager: true, schemaManager: true,
}; };
public rowSelection: GuiRowSelection = { onSelectionChange(value: any[]) {
enabled: true, this.selected_rows = value;
type: GuiRowSelectionType.CHECKBOX, this.Selectedrows = value.map(item => item.id);
mode: GuiRowSelectionMode.MULTIPLE, this.rows = value; // For legacy compat if needed
}; }
ngOnInit(): void { ngOnInit(): void {
this.selected_group = Number(this.route.snapshot.paramMap.get("id")); this.selected_group = Number(this.route.snapshot.paramMap.get("id"));
@ -177,8 +164,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
} }
single_device_action(dev: any, action: string) { single_device_action(dev: any, action: string) {
const api: GuiGridApi = this.gridComponent.api; this.selected_rows = [];
api.unselectAll();
this.Selectedrows = [dev["id"]]; this.Selectedrows = [dev["id"]];
switch (action) { switch (action) {
case "edit": case "edit":
@ -273,10 +259,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
}); });
} }
onSelectedRows(rows: Array<GuiSelectedRow>): void { // Removed legacy onSelectedRows
this.rows = rows;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
}
checkvalid(type: string): boolean { checkvalid(type: string): boolean {
var rx = var rx =

View file

@ -22,7 +22,11 @@ import {
import { MatMenuModule } from "@angular/material/menu"; import { MatMenuModule } from "@angular/material/menu";
import { DevicesRoutingModule } from "./devices-routing.module"; import { DevicesRoutingModule } from "./devices-routing.module";
import { DevicesComponent } from "./devices.component"; import { DevicesComponent } from "./devices.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PrimeNGTableModule } from 'primeng/table';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { MultiSelectModule } from 'primeng/multiselect';
import { TooltipModule as PrimeNGTooltipModule } from 'primeng/tooltip';
@NgModule({ @NgModule({
imports: [ imports: [
@ -34,7 +38,6 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule,
NavbarModule, NavbarModule,
CollapseModule, CollapseModule,
DropdownModule, DropdownModule,
@ -46,6 +49,11 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
MatMenuModule, MatMenuModule,
TooltipModule, TooltipModule,
TableModule, TableModule,
PrimeNGTableModule,
CheckboxModule,
InputTextModule,
MultiSelectModule,
PrimeNGTooltipModule
], ],
declarations: [DevicesComponent], declarations: [DevicesComponent],
}) })

View file

@ -13,82 +13,119 @@
</c-row> </c-row>
</c-card-header> </c-card-header>
<c-card-body> <c-card-body>
<gui-grid [source]="source" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <div class="mb-1 d-flex justify-content-end">
[autoResizeWidth]=true> <span class="p-input-icon-left table-search-input">
<gui-grid-column header="Name" field="name"> <i class="pi pi-search"></i>
<ng-template let-value="item.name" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search groups..." (input)="applyFilterGlobal($event, 'contains')"
{{value}} class="form-control-sm" />
</ng-template> </span>
</gui-grid-column> </div>
<gui-grid-column header="Devices" field="array_agg" align="CENTER">
<ng-template let-value="item.array_agg" let-item="item" let-index="index">
<ng-container *ngIf="item.id==1 ; then Default;else NotDefault">
</ng-container>
<ng-template #Default>
<c-badge color="info"><i class="fa-solid fa-network-wired me-1"></i>All Devices</c-badge>
</ng-template>
<ng-template #NotDefault>
<c-badge color="info" *ngIf="value[0]==null && item.id!=1"
[cTooltip]="'No devices assigned'" style="cursor: help">
<i class="fa-solid fa-server me-1"></i>0
</c-badge>
<c-badge color="info" *ngIf="value[0]!=null"
[cTooltip]="getDevicesTooltip(item)"
[cTooltipPlacement]="'top'" style="cursor: help">
<i class="fa-solid fa-server me-1"></i>{{value.length}}
</c-badge>
</ng-template>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Create Time" field="created">
<ng-template let-value="item.created" let-item="item" let-index="index">
{{formatCreateTime(value)}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Users" field="assigned_users" width="80" align="CENTER"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<ng-template let-value="item.assigned_users" let-item="item" let-index="index"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
<c-badge color="info" [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[cTooltip]="getUsersTooltip(value)" [globalFilterFields]="['name']" [loading]="loading">
[cTooltipPlacement]="'top'"
style="cursor: help">
<i class="fa-solid fa-users me-1"></i>{{value.length}}
</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" [cellEditing]="false" [sorting]="false" header="Action"> <ng-template pTemplate="header">
<ng-template let-value="value" let-item="item"> <tr>
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary" (click)="show_members(item.id)" <th pSortableColumn="name" pResizableColumn>
style="border: none;padding: 4px 7px;"><i class="fa-regular fa-eye"></i><small> View devices</small> <div class="justify-between">
</button> <span>Name</span>
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;" <span>
[matMenuTriggerFor]="menu" cButton> <p-sortIcon field="name"></p-sortIcon>
<i class="fa-solid fa-bars"></i> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</button> </span>
<mat-menu #menu="matMenu">
<div cListGroup>
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
<button size="sm" (click)="editAddGroup(item,'showedit')" style="padding: 4px 7px;"
[disabled]="item.id==1" cListGroupItem><i class="fa-solid fa-pencil"></i><small>
Edit Group</small></button>
<button size="sm" (click)="manageUsers(item)" style="padding: 4px 7px;"
cListGroupItem><i class="fa-solid fa-users-gear"></i><small>
Manage Users</small></button>
<button size="sm" (click)="groupFirmwareAction(item, 'update')" style="padding: 4px 7px;"
cListGroupItem><i class="text-success fa-solid fa-upload"></i><small>
Update Firmware</small></button>
<button size="sm" (click)="groupFirmwareAction(item, 'upgrade')" style="padding: 4px 7px;"
cListGroupItem><i class="text-secondary fa-solid fa-microchip"></i><small>
Upgrade Firmware</small></button>
<button size="sm" (click)="show_delete_group(item)" style="padding: 4px 7px;"
[disabled]="item.id==1" cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
Delete Group</small></button>
</div> </div>
</mat-menu> </th>
</ng-template> <th pSortableColumn="description" pResizableColumn>
</gui-grid-column> <div class="justify-between">
</gui-grid> <span>Member Devices</span>
<span>
<p-sortIcon field="description"></p-sortIcon>
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="pair_type" pResizableColumn>
<div class="justify-between">
<span>Created</span>
<span>
<p-sortIcon field="pair_type"></p-sortIcon>
<p-columnFilter type="text" field="pair_type" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="member_count" pResizableColumn>
<div class="justify-between">
<span>Member Users</span>
<span>
<p-sortIcon field="member_count"></p-sortIcon>
<p-columnFilter type="numeric" field="member_count" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><strong>{{item.name}}</strong></td>
<td class="text-center">
<ng-container *ngIf="item.id==1 ; then Default; else NotDefault"></ng-container>
<ng-template #Default>
<h6><c-badge color="info"><i class="fa-solid fa-network-wired me-1"></i>All Devices</c-badge></h6>
</ng-template>
<ng-template #NotDefault>
<c-badge color="info" *ngIf="(!item.array_agg || item.array_agg[0]==null) && item.id!=1"
pTooltip="No devices assigned" style="cursor: help">
<i class="fa-solid fa-server me-1"></i>0
</c-badge>
<h6><c-badge color="info" *ngIf="item.array_agg && item.array_agg[0]!=null"
[pTooltip]="getDevicesTooltip(item)" style="cursor: help">
<i class="fa-solid fa-server me-1"></i>{{item.array_agg.length}}
</c-badge></h6>
</ng-template>
</td>
<td>{{formatCreateTime(item.created)}}</td>
<td class="text-center">
<h6><c-badge color="info" [pTooltip]="getUsersTooltip(item.assigned_users)" style="cursor: help">
<i class="fa-solid fa-users me-1"></i>{{item.assigned_users?.length || 0}}
</c-badge></h6>
</td>
<td class="text-center">
<div class="d-flex justify-content-center">
<button size="sm" variant="outline" cButton color="primary" (click)="show_members(item.id)"
style="border: none;padding: 4px 7px;" pTooltip="View devices"><i class="fa-regular fa-eye"></i>
</button>
<button color="primary" variant="ghost" style="padding: 4px 7px;" [matMenuTriggerFor]="menu" cButton>
<i class="fa-solid fa-bars"></i>
</button>
<mat-menu #menu="matMenu">
<div cListGroup>
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
<button size="sm" (click)="editAddGroup(item,'showedit')" style="padding: 4px 7px;"
[disabled]="item.id==1" cListGroupItem><i class="fa-solid fa-pencil"></i><small>
Edit Group</small></button>
<button size="sm" (click)="manageUsers(item)" style="padding: 4px 7px;" cListGroupItem><i
class="fa-solid fa-users-gear"></i><small>
Manage Users</small></button>
<button size="sm" (click)="groupFirmwareAction(item, 'update')" style="padding: 4px 7px;"
cListGroupItem><i class="text-success fa-solid fa-upload"></i><small>
Update Firmware</small></button>
<button size="sm" (click)="groupFirmwareAction(item, 'upgrade')" style="padding: 4px 7px;"
cListGroupItem><i class="text-secondary fa-solid fa-microchip"></i><small>
Upgrade Firmware</small></button>
<button size="sm" (click)="show_delete_group(item)" style="padding: 4px 7px;"
[disabled]="item.id==1" cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
Delete Group</small></button>
</div>
</mat-menu>
</div>
</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
@ -134,30 +171,54 @@
<p class="mb-0">Click "Add Devices" to start adding devices to this group</p> <p class="mb-0">Click "Add Devices" to start adding devices to this group</p>
</div> </div>
<div *ngIf="groupMembers.length > 0"> <div *ngIf="groupMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [searching]="searching" [source]="groupMembers" [columnMenu]="columnMenu" <div class="mb-3 d-flex justify-content-end">
[sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection" <span class="p-input-icon-left table-search-input">
(selectedRows)="onSelectedRowsMembers($event)" [paging]="paging"> <i class="pi pi-search"></i>
<gui-grid-column header="Device Name" field="name"> <input type="text" pInputText placeholder="Search members..."
<ng-template let-value="item.name" let-item="item" let-index="index"> (input)="applyFilterMembers($event, 'contains')" class="form-control-sm" />
<div class="d-flex align-items-center"> </span>
<i class="fa-solid fa-server me-2 text-primary"></i> </div>
<strong>{{value}}</strong>
</div> <p-table #dtMembers [value]="groupMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
</ng-template> [stripedRows]="true" styleClass="p-datatable-sm" [(selection)]="MemberRows"
</gui-grid-column> (selectionChange)="onSelectedRowsMembers($event)" [globalFilterFields]="['name', 'ip']">
<gui-grid-column header="IP Address" field="ip"> <ng-template pTemplate="header">
<ng-template let-value="item.ip" let-item="item" let-index="index"> <tr>
<c-badge color="secondary">{{value}}</c-badge> <th style="width: 4rem"><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
</ng-template> <th pSortableColumn="name" pResizableColumn>
</gui-grid-column> <div class="justify-between">
<gui-grid-column header="Actions" width="100" field="action" align="CENTER"> <span>Name</span>
<ng-template let-value="item.id" let-item="item" let-index="index"> <p-sortIcon field="name"></p-sortIcon>
<button cButton color="danger" size="sm" variant="outline" (click)="remove_from_group(item.id)" title="Remove from group"> </div>
<i class="fa-solid fa-times"></i> </th>
</button> <th pSortableColumn="mac" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> <span>MAC Address</span>
</gui-grid> <p-sortIcon field="mac"></p-sortIcon>
</div>
</th>
<th style="width: 100px" class="text-center">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
<td>
<div class="d-flex align-items-center">
<i class="fa-solid fa-server me-2 text-primary"></i>
<strong>{{item.name}}</strong>
</div>
</td>
<td><c-badge color="secondary">{{item.ip}}</c-badge></td>
<td class="text-center">
<button cButton color="danger" size="sm" variant="outline" (click)="remove_from_group(item.id)"
pTooltip="Remove from group">
<i class="fa-solid fa-times"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div> </div>
</c-card-body> </c-card-body>
</c-card> </c-card>
@ -172,7 +233,8 @@
</c-modal-footer> </c-modal-footer>
</c-modal> </c-modal>
<c-modal #NewMemberModal [backdrop]="true" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal" style="z-index: 1060; backdrop-filter: blur(2px);"> <c-modal #NewMemberModal [backdrop]="true" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal"
style="z-index: 1060; backdrop-filter: blur(2px);">
<c-modal-header class="bg-success text-white"> <c-modal-header class="bg-success text-white">
<h5 cModalTitle><i class="fa-solid fa-plus-circle me-2"></i>Add Devices to Group</h5> <h5 cModalTitle><i class="fa-solid fa-plus-circle me-2"></i>Add Devices to Group</h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button> <button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
@ -189,7 +251,8 @@
<!-- Available Devices --> <!-- Available Devices -->
<c-card> <c-card>
<c-card-header> <c-card-header>
<h6 class="mb-0"><i class="fa-solid fa-server me-2"></i>Available Devices ({{availbleMembers.length}} total)</h6> <h6 class="mb-0"><i class="fa-solid fa-server me-2"></i>Available Devices ({{availbleMembers.length}} total)
</h6>
</c-card-header> </c-card-header>
<c-card-body class="p-0"> <c-card-body class="p-0">
<div *ngIf="availbleMembers.length === 0" class="text-center p-4 text-muted"> <div *ngIf="availbleMembers.length === 0" class="text-center p-4 text-muted">
@ -198,28 +261,45 @@
<p class="mb-0">No available devices to add to this group</p> <p class="mb-0">No available devices to add to this group</p>
</div> </div>
<div *ngIf="availbleMembers.length > 0"> <div *ngIf="availbleMembers.length > 0">
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching" <div class="mb-3 d-flex justify-content-end w-100">
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left table-search-input">
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging"> <i class="pi pi-search"></i>
<gui-grid-column header="Device Name" field="name"> <input type="text" pInputText placeholder="Search available..."
<ng-template let-value="item.name" let-item="item" let-index="index"> (input)="applyFilterNewMembers($event, 'contains')" class="form-control-sm" />
<div class="d-flex align-items-center"> </span>
<i class="fa-solid fa-server me-2 text-primary"></i> </div>
<strong>{{value}}</strong>
</div> <p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [resizableColumns]="true"
</ng-template> columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
</gui-grid-column> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped" [(selection)]="NewMemberRows"
<gui-grid-column header="IP Address" field="ip"> (selectionChange)="onSelectedRowsNewMembers($event)" [globalFilterFields]="['name', 'ip', 'mac']">
<ng-template let-value="item.ip" let-item="item" let-index="index"> <ng-template pTemplate="header">
<c-badge color="secondary">{{value}}</c-badge> <tr>
</ng-template> <th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
</gui-grid-column> <th pSortableColumn="name" pResizableColumn>Device Name <p-sortIcon
<gui-grid-column header="MAC Address" field="mac"> field="name"></p-sortIcon><p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
<ng-template let-value="item.mac" let-item="item" let-index="index"> </th>
<small class="text-muted">{{value}}</small> <th pSortableColumn="ip" pResizableColumn>IP Address <p-sortIcon field="ip"></p-sortIcon><p-columnFilter
</ng-template> type="text" field="ip" display="menu" class="ms-auto" /></th>
</gui-grid-column> <th pSortableColumn="mac" pResizableColumn>MAC Address <p-sortIcon
</gui-grid> field="mac"></p-sortIcon><p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
<td>
<div class="d-flex align-items-center">
<i class="fa-solid fa-server me-2 text-primary"></i>
<strong>{{item.name}}</strong>
</div>
</td>
<td><c-badge color="secondary">{{item.ip}}</c-badge></td>
<td><small class="text-muted">{{item.mac}}</small></td>
</tr>
</ng-template>
</p-table>
</div> </div>
</c-card-body> </c-card-body>
</c-card> </c-card>
@ -268,7 +348,8 @@
</button> </button>
</c-modal-footer> </c-modal-footer>
</c-modal> </c-modal>
<c-modal #UserManagementModal backdrop="static" size="xl" [(visible)]="UserManagementModalVisible" id="UserManagementModal"> <c-modal #UserManagementModal backdrop="static" size="xl" [(visible)]="UserManagementModalVisible"
id="UserManagementModal">
<c-modal-header> <c-modal-header>
<h5 cModalTitle><i class="fa-solid fa-users-gear me-2"></i>User Permissions - {{selectedGroup?.name}}</h5> <h5 cModalTitle><i class="fa-solid fa-users-gear me-2"></i>User Permissions - {{selectedGroup?.name}}</h5>
<button [cModalToggle]="UserManagementModal.id" cButtonClose></button> <button [cModalToggle]="UserManagementModal.id" cButtonClose></button>
@ -283,18 +364,11 @@
<c-col md="4"> <c-col md="4">
<div class="search-select-wrapper"> <div class="search-select-wrapper">
<label class="search-label">User</label> <label class="search-label">User</label>
<input cFormControl <input cFormControl [(ngModel)]="userSearch" (input)="filterUsers($event)"
[(ngModel)]="userSearch" (focus)="showUserDropdown = true" (blur)="hideUserDropdown()" placeholder="Search and select user..."
(input)="filterUsers($event)" class="search-input compact-select" autocomplete="off" />
(focus)="showUserDropdown = true"
(blur)="hideUserDropdown()"
placeholder="Search and select user..."
class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showUserDropdown && filteredUsers.length > 0" class="search-dropdown"> <div *ngIf="showUserDropdown && filteredUsers.length > 0" class="search-dropdown">
<div *ngFor="let user of filteredUsers" <div *ngFor="let user of filteredUsers" class="search-option" (mousedown)="selectUser(user)">
class="search-option"
(mousedown)="selectUser(user)">
{{user.username}} ({{user.first_name}} {{user.last_name}}) {{user.username}} ({{user.first_name}} {{user.last_name}})
</div> </div>
</div> </div>
@ -306,28 +380,24 @@
<c-col md="4"> <c-col md="4">
<div class="search-select-wrapper"> <div class="search-select-wrapper">
<label class="search-label">Permission</label> <label class="search-label">Permission</label>
<input cFormControl <input cFormControl [(ngModel)]="permissionSearch" (input)="filterPermissions($event)"
[(ngModel)]="permissionSearch" (focus)="showPermissionDropdown = true" (blur)="hidePermissionDropdown()"
(input)="filterPermissions($event)" placeholder="Search and select permission..." class="search-input compact-select" autocomplete="off" />
(focus)="showPermissionDropdown = true"
(blur)="hidePermissionDropdown()"
placeholder="Search and select permission..."
class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown"> <div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
<div *ngFor="let perm of filteredPermissions" <div *ngFor="let perm of filteredPermissions" class="search-option"
class="search-option"
(mousedown)="selectPermission(perm)"> (mousedown)="selectPermission(perm)">
{{perm.name}} {{perm.name}}
</div> </div>
</div> </div>
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch" class="search-no-results"> <div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch"
class="search-no-results">
No permissions found No permissions found
</div> </div>
</div> </div>
</c-col> </c-col>
<c-col md="4" class="d-flex align-items-end"> <c-col md="4" class="d-flex align-items-end">
<button cButton color="success" (click)="addUserPermission()" [disabled]="!selectedUser || !selectedPermission"> <button cButton color="success" (click)="addUserPermission()"
[disabled]="!selectedUser || !selectedPermission">
<i class="fa-solid fa-plus me-1"></i>Add Permission <i class="fa-solid fa-plus me-1"></i>Add Permission
</button> </button>
</c-col> </c-col>
@ -336,7 +406,8 @@
</c-card> </c-card>
<c-card> <c-card>
<c-card-header class="bg-light"> <c-card-header class="bg-light">
<h6 class="mb-0"><i class="fa-solid fa-list-check me-2"></i>Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)</h6> <h6 class="mb-0"><i class="fa-solid fa-list-check me-2"></i>Current Permissions
({{selectedGroup?.assigned_users?.length || 0}} users)</h6>
</c-card-header> </c-card-header>
<c-card-body class="p-0"> <c-card-body class="p-0">
<div *ngIf="selectedGroup?.assigned_users?.length === 0" class="text-center p-4 text-muted"> <div *ngIf="selectedGroup?.assigned_users?.length === 0" class="text-center p-4 text-muted">
@ -369,14 +440,12 @@
</td> </td>
<td> <td>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button cButton color="warning" size="sm" variant="outline" <button cButton color="warning" size="sm" variant="outline" (click)="editUserPermission(user)"
(click)="editUserPermission(user)" title="Change Permission" title="Change Permission" [disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<i class="fa-solid fa-edit"></i> <i class="fa-solid fa-edit"></i>
</button> </button>
<button cButton color="danger" size="sm" variant="outline" <button cButton color="danger" size="sm" variant="outline" (click)="removeUserPermission(user)"
(click)="removeUserPermission(user)" title="Remove Permission" title="Remove Permission" [disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</button> </button>
</div> </div>
@ -402,7 +471,8 @@
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Current Permission: <c-badge [color]="getPermissionColor(editingUser?.perm_name)">{{editingUser?.perm_name}}</c-badge></label> <label class="form-label">Current Permission: <c-badge
[color]="getPermissionColor(editingUser?.perm_name)">{{editingUser?.perm_name}}</c-badge></label>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">New Permission</label> <label class="form-label">New Permission</label>
@ -430,7 +500,9 @@
<c-modal-body> <c-modal-body>
<div class="text-center"> <div class="text-center">
<i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i> <i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i>
<p>Are you sure you want to remove <strong>{{removingUser?.username}}</strong>'s permission from group <strong>{{selectedGroup?.name}}</strong>?</p> <p>Are you sure you want to remove <strong>{{removingUser?.username}}</strong>'s permission from group
<strong>{{selectedGroup?.name}}</strong>?
</p>
<p class="text-muted small">This action cannot be undone.</p> <p class="text-muted small">This action cannot be undone.</p>
</div> </div>
</c-modal-body> </c-modal-body>
@ -451,13 +523,17 @@
<c-modal-body> <c-modal-body>
<div class="text-center"> <div class="text-center">
<i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i> <i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i>
<p>Are you sure you want to <strong>{{firmwareAction}}</strong> firmware for all devices in group <strong>{{selectedGroupForFirmware?.name}}</strong>?</p> <p>Are you sure you want to <strong>{{firmwareAction}}</strong> firmware for all devices in group
<p class="text-muted small">This action will affect all devices in this group and may take some time to complete.</p> <strong>{{selectedGroupForFirmware?.name}}</strong>?
</p>
<p class="text-muted small">This action will affect all devices in this group and may take some time to complete.
</p>
</div> </div>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
<button cButton color="primary" (click)="confirmGroupFirmwareAction()"> <button cButton color="primary" (click)="confirmGroupFirmwareAction()">
<i class="fa-solid fa-{{firmwareAction === 'update' ? 'upload' : 'microchip'}} me-1"></i>{{firmwareAction | titlecase}} Firmware <i class="fa-solid fa-{{firmwareAction === 'update' ? 'upload' : 'microchip'}} me-1"></i>{{firmwareAction |
titlecase}} Firmware
</button> </button>
<button [cModalToggle]="FirmwareConfirmModal.id" cButton color="secondary"> <button [cModalToggle]="FirmwareConfirmModal.id" cButton color="secondary">
Cancel Cancel

View file

@ -1,20 +1,9 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { import { Table } from 'primeng/table';
GuiSearching,
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
interface IUser { interface IUser {
name: string; name: string;
@ -35,9 +24,13 @@ interface IUser {
styleUrls: ["devgroup.component.scss"] styleUrls: ["devgroup.component.scss"]
}) })
export class DevicesGroupComponent implements OnInit { export class DevicesGroupComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public tz: string; public tz: string = '';
@ViewChild('dt') table!: Table;
@ViewChild('dtMembers') tableMembers!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
@ -68,7 +61,6 @@ export class DevicesGroupComponent implements OnInit {
} }
} }
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public MemberRows: any = []; public MemberRows: any = [];
public NewMemberRows: any = []; public NewMemberRows: any = [];
@ -117,42 +109,17 @@ export class DevicesGroupComponent implements OnInit {
id: 0, id: 0,
name: "", name: "",
}; };
public sorting = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
multiSorting: true, }
};
searching: GuiSearching = { applyFilterMembers($event: any, stringVal: string) {
enabled: true, this.tableMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
placeholder: "Search Devices", }
};
public paging: GuiPaging = { applyFilterNewMembers($event: any, stringVal: string) {
enabled: true, this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
page: 1, }
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
ngOnInit(): void { ngOnInit(): void {
this.initGridTable(); this.initGridTable();
@ -175,13 +142,13 @@ export class DevicesGroupComponent implements OnInit {
}); });
} }
onSelectedRowsMembers(rows: Array<GuiSelectedRow>): void { onSelectedRowsMembers(rows: any[]): void {
this.MemberRows = rows; this.MemberRows = rows;
this.SelectedMemberRows = rows.map((m: GuiSelectedRow) => m.source.id); this.SelectedMemberRows = rows.map((m: any) => m.id);
} }
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void { onSelectedRowsNewMembers(rows: any[]): void {
this.NewMemberRows = rows; this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source.id); this.SelectedNewMemberRows = rows.map((m: any) => m.id);
} }
add_new_members() { add_new_members() {
var _self = this; var _self = this;
@ -193,7 +160,7 @@ export class DevicesGroupComponent implements OnInit {
this.groupMembers = [ this.groupMembers = [
...new Set( ...new Set(
this.groupMembers.concat( this.groupMembers.concat(
this.NewMemberRows.map((m: GuiSelectedRow) => m.source) this.NewMemberRows
) )
), ),
]; ];

View file

@ -15,7 +15,9 @@ import {
} from "@coreui/angular"; } from "@coreui/angular";
import { DevicesGroupRoutingModule } from "./devgroup-routing.module"; import { DevicesGroupRoutingModule } from "./devgroup-routing.module";
import { DevicesGroupComponent } from "./devgroup.component"; import { DevicesGroupComponent } from "./devgroup.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { BadgeModule } from "@coreui/angular"; import { BadgeModule } from "@coreui/angular";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { MatMenuModule } from "@angular/material/menu"; import { MatMenuModule } from "@angular/material/menu";
@ -30,7 +32,9 @@ import { MatMenuModule } from "@angular/material/menu";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
CollapseModule, CollapseModule,
ModalModule, ModalModule,
BadgeModule, BadgeModule,

View file

@ -1,10 +1,28 @@
<div style="position: relative;">
<c-row class="network-container"> <c-row class="network-container">
<c-col [xs]="12" class="network-col"> <c-col [xs]="12" class="network-col">
<c-card class="network-card"> <c-card class="network-card">
<button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn"> <div class="map-actions">
<i class="fas fa-sync-alt"></i> Refresh <button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn">
</button> <i class="fas fa-sync-alt"></i> Refresh
<div #network class="network-canvas"></div> </button>
<button cButton color="warning" size="sm" (click)="resetLocations()" class="refresh-btn">
<i class="fas fa-thumbtack"></i> Reset Locations
</button>
<button cButton color="danger" size="sm" (click)="showResetModal = true" class="refresh-btn">
<i class="fas fa-trash-alt"></i> Reset Map
</button>
</div>
<div class="network-canvas">
<div #network class="full-size"></div>
<div *ngIf="loadingMap && mikrotikData.length === 0" class="map-loading-overlay">
<div class="loader-content">
<i class="fas fa-project-diagram fa-spin"></i>
<h3>Generating Map...</h3>
<p>Please wait while we discover your network topology. This might take a few minutes. (Polling every 30s)</p>
</div>
</div>
</div>
</c-card> </c-card>
</c-col> </c-col>
</c-row> </c-row>
@ -80,3 +98,21 @@
<button cButton color="secondary" (click)="closeWebAccessModal()">Cancel</button> <button cButton color="secondary" (click)="closeWebAccessModal()">Cancel</button>
</c-modal-footer> </c-modal-footer>
</c-modal> </c-modal>
<!-- Reset Map Confirmation Modal -->
<c-modal #ResetMapModal backdrop="static" [(visible)]="showResetModal" id="ResetMapModal">
<c-modal-header>
<h6 cModalTitle>Confirm Map Reset</h6>
<button cButtonClose (click)="showResetModal = false"></button>
</c-modal-header>
<c-modal-body>
<p>Are you sure you want to reset all network discovery data?</p>
<p class="text-danger"><i class="fas fa-exclamation-triangle"></i> This will delete all current neighbors' data and trigger a fresh scan. Node positions will also be reset.</p>
</c-modal-body>
<c-modal-footer>
<button cButton color="secondary" (click)="showResetModal = false">Cancel</button>
<button cButton color="danger" (click)="confirmResetMap()">Reset Everything</button>
</c-modal-footer>
</c-modal>
<app-license-expired-overlay></app-license-expired-overlay>
</div>

View file

@ -18,13 +18,24 @@
position: relative; position: relative;
} }
.refresh-btn { .map-actions {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 15px;
z-index: 1000; z-index: 1000;
display: flex;
gap: 8px;
align-items: center;
}
.refresh-btn {
padding: 6px 12px; padding: 6px 12px;
font-size: 12px; font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
white-space: nowrap;
} }
.network-canvas { .network-canvas {
@ -34,6 +45,48 @@
border-radius: 8px; border-radius: 8px;
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0,0,0,0.1);
position: relative;
overflow: hidden;
.full-size {
width: 100%;
height: 100%;
}
.map-loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(4px);
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
.loader-content {
i {
font-size: 4rem;
color: #3498db;
margin-bottom: 20px;
}
h3 {
color: #2c3e50;
font-weight: 600;
margin-bottom: 10px;
}
p {
color: #7f8c8d;
max-width: 400px;
margin: 0 auto;
}
}
}
::ng-deep .vis-network { ::ng-deep .vis-network {
cursor: default; cursor: default;

View file

@ -10,7 +10,7 @@ import { DataSet } from 'vis-data';
templateUrl: "maps.component.html", templateUrl: "maps.component.html",
styleUrls: ["maps.component.scss"], styleUrls: ["maps.component.scss"],
}) })
export class MapsComponent implements OnInit { export class MapsComponent implements OnInit, OnDestroy {
public uid: number; public uid: number;
public uname: string; public uname: string;
public ispro: boolean = false; public ispro: boolean = false;
@ -19,8 +19,12 @@ export class MapsComponent implements OnInit {
public savedPositionsKey = "network-layout"; public savedPositionsKey = "network-layout";
public selectedDevice: any = null; public selectedDevice: any = null;
public showWebAccessModal: boolean = false; public showWebAccessModal: boolean = false;
public showMoreInfoModal: boolean = false;
public currentDeviceInfo: any = null; public currentDeviceInfo: any = null;
public showResetModal: boolean = false;
public loadingMap: boolean = false;
private pollingTimer: any;
private isDestroyed: boolean = false;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
private router: Router, private router: Router,
@ -54,13 +58,38 @@ export class MapsComponent implements OnInit {
this.loadNetworkData(); this.loadNetworkData();
} }
ngOnDestroy(): void {
this.isDestroyed = true;
if (this.pollingTimer) {
clearTimeout(this.pollingTimer);
}
}
loadNetworkData(): void { loadNetworkData(): void {
if (this.isDestroyed) return;
clearTimeout(this.pollingTimer);
this.loadingMap = true;
this.data_provider.getNetworkMap().then((res) => { this.data_provider.getNetworkMap().then((res) => {
this.mikrotikData = res; if (this.isDestroyed) return;
console.dir(res); // Normalize response - handle array or object with 'result' property
setTimeout(() => { const data = (res && res.result) ? res.result : res;
this.createNetworkMap();
}, 100); if (Array.isArray(data) && data.length > 0) {
this.loadingMap = false;
this.mikrotikData = data;
setTimeout(() => {
this.createNetworkMap();
}, 100);
} else {
console.log("Map data is empty, likely generating. Retrying in 30s...");
this.mikrotikData = [];
this.pollingTimer = setTimeout(() => {
this.loadNetworkData();
}, 30000);
}
}).catch(err => {
console.error("Error loading network map:", err);
this.loadingMap = false;
}); });
} }
@ -476,4 +505,31 @@ Object.entries(connectionMap).forEach(([connectionKey, interfacePairs]) => {
// Implement configuration interface // Implement configuration interface
} }
resetLocations() {
localStorage.removeItem(this.savedPositionsKey);
this.savedPositions = {};
this.refreshData();
}
confirmResetMap() {
this.showResetModal = false;
this.loadingMap = true;
this.mikrotikData = [];
this.selectedDevice = null;
this.data_provider.resetNetworkMap().then((res) => {
if (res.status === 'success') {
localStorage.removeItem(this.savedPositionsKey);
this.savedPositions = {};
this.loadNetworkData();
} else {
this.loadingMap = false;
alert("Error: " + (res.error || "Failed to reset map"));
}
}).catch(err => {
this.loadingMap = false;
alert("Error resetting map: " + err);
});
}
} }

View file

@ -27,6 +27,7 @@ import { MapsRoutingModule } from "./maps-routing.module";
import { MapsComponent } from "./maps.component"; import { MapsComponent } from "./maps.component";
import { ClipboardModule } from "@angular/cdk/clipboard"; import { ClipboardModule } from "@angular/cdk/clipboard";
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { SharedModule } from "../../shared/shared.module";
@NgModule({ @NgModule({
imports: [ imports: [
@ -53,7 +54,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
TooltipModule, TooltipModule,
UtilitiesModule, UtilitiesModule,
InfiniteScrollModule, InfiniteScrollModule,
IconModule IconModule,
SharedModule
], ],
declarations: [MapsComponent], declarations: [MapsComponent],
}) })

View file

@ -1,3 +1,4 @@
<div style="position: relative;">
<c-row style="height: calc(100vh - 10rem);" (click)="disableContextMenu()" > <c-row style="height: calc(100vh - 10rem);" (click)="disableContextMenu()" >
<c-col xs="3" style="height:100%;"> <c-col xs="3" style="height:100%;">
<c-card style="height:100%"> <c-card style="height:100%">
@ -176,6 +177,8 @@
</c-card> </c-card>
</c-col> </c-col>
</c-row> </c-row>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<div *ngIf="contextmenu"> <div *ngIf="contextmenu">
<div class="contextmenu" [ngStyle]="{'left.px': contextmenuX, 'top.px': contextmenuY}"> <div class="contextmenu" [ngStyle]="{'left.px': contextmenuX, 'top.px': contextmenuY}">
<c-card style="padding: 1px;"> <c-card style="padding: 1px;">

View file

@ -25,6 +25,7 @@ import { MonitoringRoutingModule } from "./monitoring-routing.module";
import { MonitoringComponent } from "./monitoring.component"; import { MonitoringComponent } from "./monitoring.component";
import { ClipboardModule } from "@angular/cdk/clipboard"; import { ClipboardModule } from "@angular/cdk/clipboard";
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { SharedModule } from "../../shared/shared.module";
@NgModule({ @NgModule({
imports: [ imports: [
@ -49,7 +50,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
TableModule, TableModule,
TooltipModule, TooltipModule,
UtilitiesModule, UtilitiesModule,
InfiniteScrollModule InfiniteScrollModule,
SharedModule
], ],
declarations: [MonitoringComponent], declarations: [MonitoringComponent],
}) })

View file

@ -13,30 +13,66 @@
</c-row> </c-row>
</c-card-header> </c-card-header>
<c-card-body> <c-card-body>
<gui-grid [rowHeight]="82" [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" <div class="mb-3 d-flex justify-content-end">
[sorting]="sorting" [autoResizeWidth]=true [paging]="paging"> <span class="p-input-icon-left">
<gui-grid-column header="Name" field="name"> <i class="pi pi-search"></i>
<ng-template let-value="item.name" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search permissions..." (input)="applyFilterGlobal($event, 'contains')"
&nbsp; {{value}} </ng-template> class="form-control-sm" />
</gui-grid-column> </span>
<gui-grid-column width="auto" header="Perms" field="perms"> </div>
<ng-template let-value="item.role" let-item="item" let-index="index">
<div style="text-wrap: initial;"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<ng-container *ngFor="let perm of item['perms'] | keyvalue"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
<c-badge *ngIf="perm.value" class="m-1" color="success">{{perm.key}}</c-badge> [showGridlines]="true" [stripedRows]="true"
</ng-container> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</div> [globalFilterFields]="['name']"
</ng-template> [loading]="loading">
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action"> <ng-template pTemplate="header">
<ng-template let-value="item.id" let-item="item" let-index="index"> <tr>
<button cButton color="warning" size="sm" class="mx-1" (click)="editAddTask(item,'edit');"><i <th pSortableColumn="name" pResizableColumn>
class="fa-regular fa-pen-to-square"></i></button> <div class="justify-between">
<button cButton color="danger" size="sm" (click)="confirm_delete(item);"><i <span>Permission Name</span>
class="fa-regular fa-trash-can"></i></button> <span>
</ng-template> <p-sortIcon field="name"></p-sortIcon>
</gui-grid-column> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</gui-grid> </span>
</div>
</th>
<th pResizableColumn>Perms</th>
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>{{item.name}}</td>
<td>
<div class="d-flex flex-wrap gap-1">
<ng-container *ngFor="let perm of item['perms'] | keyvalue">
<c-badge *ngIf="perm.value" color="success">{{perm.key}}</c-badge>
</ng-container>
</div>
</td>
<td class="text-center">
<div class="d-flex gap-1 justify-content-center">
<button cButton color="warning" size="sm" (click)="editAddTask(item,'edit');" pTooltip="Edit Permission">
<i class="fa-regular fa-pen-to-square"></i>
</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item);" pTooltip="Delete Permission">
<i class="fa-regular fa-trash-can"></i>
</button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="3" class="text-center p-4">No permissions found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>

View file

@ -1,13 +1,8 @@
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core"; import { Component, OnInit, QueryList, ViewChildren, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
} from "@generic-ui/ngx-grid";
import { ToasterComponent } from "@coreui/angular"; import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component"; import { AppToastComponent } from "../toast-simple/toast.component";
@ -30,8 +25,10 @@ interface IUser {
templateUrl: "permissions.component.html", templateUrl: "permissions.component.html",
}) })
export class PermissionsComponent implements OnInit { export class PermissionsComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
@ViewChild('dt') table!: Table;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
@ -63,7 +60,6 @@ export class PermissionsComponent implements OnInit {
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public SelectedPerm: any = {}; public SelectedPerm: any = {};
@ -107,24 +103,9 @@ export class PermissionsComponent implements OnInit {
closeButton: true, closeButton: true,
}; };
public sorting = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
multiSorting: true, }
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
show_toast(title: string, body: string, color: string) { show_toast(title: string, body: string, color: string) {
const { ...props } = { ...this.toasterForm, color, title, body }; const { ...props } = { ...this.toasterForm, color, title, body };
const componentRef = this.viewChildren.first.addToast( const componentRef = this.viewChildren.first.addToast(

View file

@ -18,7 +18,9 @@ import { IconModule } from "@coreui/icons-angular";
import { PermissionsRoutingModule } from "./permissions-routing.module"; import { PermissionsRoutingModule } from "./permissions-routing.module";
import { PermissionsComponent } from "./permissions.component"; import { PermissionsComponent } from "./permissions.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule } from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { TooltipModule } from 'primeng/tooltip';
@NgModule({ @NgModule({
imports: [ imports: [
@ -33,7 +35,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, TableModule,
InputTextModule,
TooltipModule,
ModalModule, ModalModule,
FormsModule, FormsModule,
BadgeModule, BadgeModule,

View file

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SequencesComponent } from './sequences.component';
const routes: Routes = [
{
path: '',
component: SequencesComponent,
data: {
title: 'Sequences'
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SequencesRoutingModule {
}

View file

@ -0,0 +1,712 @@
<div style="position: relative;">
<c-row>
<c-col xs>
<c-card class="mb-4">
<c-card-header>
<c-row>
<c-col xs [lg]="3">
Sequences
</c-col>
<c-col xs [lg]="9">
<h6 style="text-align: right;">
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Sequence('','add')"
style="color: #fff;"><i class="fa-solid fa-plus"></i> Create Sequence</button>
<button cButton color="warning" class="mx-1" size="sm"
(click)="ManageAlertsModalVisible = true" style="color: #000;"><i
class="fa-solid fa-bell"></i> Manage Alerts</button>
</h6>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<div class="mb-3 d-flex justify-content-end">
<span class="p-input-icon-left table-search-input">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search sequences..."
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
[showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['name', 'created']" [loading]="false">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>Name</span>
<span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="source_snippet_id" pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>Source Snippet</span>
<span>
<p-sortIcon field="source_snippet_id"></p-sortIcon>
<p-columnFilter type="numeric" field="source_snippet_id" display="menu"
class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="is_active" class="text-center" pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>Active</span>
<span>
<p-sortIcon field="is_active"></p-sortIcon>
<p-columnFilter type="boolean" field="is_active" display="menu"
class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="created" pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>Created</span>
<span>
<p-sortIcon field="created"></p-sortIcon>
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><strong>{{item.name}}</strong></td>
<td>Snippet #{{item.source_snippet_id}}</td>
<td class="text-center">
<i *ngIf="item.is_active" class="fa-solid fa-check" style="color: green;"></i>
<i *ngIf="!item.is_active" class="fa-solid fa-x" style="color: red;"></i>
</td>
<td>{{item.created}}</td>
<td class="text-center">
<div class="d-flex gap-1 justify-content-center">
<button cButton color="success" size="sm" (click)="Run_Sequence(item)"
pTooltip="Execute Sequence">
<i class="fa-solid fa-play"></i>
</button>
<button cButton color="primary" size="sm" (click)="Edit_Sequence(item,'edit')"
pTooltip="Edit Sequence">
<i class="fa-regular fa-pen-to-square"></i>
</button>
<button cButton color="info" size="sm" (click)="show_history(item)"
pTooltip="Execution History">
<i class="fa-solid fa-clock-rotate-left"></i>
</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)"
pTooltip="Delete Sequence">
<i class="fa-regular fa-trash-can"></i>
</button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="5" class="text-center p-4">No sequences found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body>
</c-card>
</c-col>
</c-row>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<c-modal #EditSequenceModal backdrop="static" size="xl" [(visible)]="EditSequenceModalVisible" id="EditSequenceModal"
class="builder-modal">
<c-modal-header class="bg-light">
<h5 *ngIf="ModalAction=='edit'" cModalTitle>
<i class="fa-solid fa-edit me-2"></i>Edit Sequence: {{current_sequence['name']}}
</h5>
<h5 *ngIf="ModalAction=='add'" cModalTitle>
<i class="fa-solid fa-plus me-2"></i>Create New Sequence
</h5>
<button [cModalToggle]="EditSequenceModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4 bg-light">
<!-- Basic Information -->
<c-card class="mb-4 shadow-sm border-0">
<c-card-header class="bg-white border-bottom-0 pt-3 pb-0">
<h6 class="mb-1"><i class="fa-solid fa-info-circle text-primary me-2"></i>Sequence Details</h6>
</c-card-header>
<c-card-body>
<c-row class="g-3 align-items-center">
<c-col xs="12" md="6">
<label class="form-label text-muted small mb-1">Sequence Name</label>
<input cFormControl placeholder="e.g. Daily Health Check" [(ngModel)]="current_sequence['name']"
class="border-secondary" />
</c-col>
<c-col xs="12" md="6">
<label class="form-label text-muted small mb-1">Source Snippet (Entry Point)</label>
<ngx-super-select [dataSource]="Snippets" [options]="snippetOptions"
(selectionChanged)="onSelectSourceSnippet($event)"
[selectedItemValues]="[current_sequence['source_snippet_id']]"
class="styled border-secondary"></ngx-super-select>
</c-col>
</c-row>
<div class="mt-3 pt-3 border-top d-flex gap-4">
<c-form-check>
<input cFormCheckInput type="checkbox" id="isActiveSeq"
[(ngModel)]="current_sequence['is_active']" />
<label cFormCheckLabel for="isActiveSeq" class="user-select-none">Sequence Active</label>
</c-form-check>
<c-form-check>
<input cFormCheckInput type="checkbox" id="storeHistorySeq"
[(ngModel)]="current_sequence['store_all_history']" />
<label cFormCheckLabel for="storeHistorySeq" class="user-select-none">Store Execution
History</label>
</c-form-check>
</div>
</c-card-body>
</c-card>
<!-- Flowchart Builder -->
<c-card class="mb-3 shadow-sm border-0 flowchart-container">
<c-card-header class="bg-white d-flex justify-content-between align-items-center py-3">
<h6 class="mb-0"><i class="fa-solid fa-project-diagram text-primary me-2"></i>Condition Flowchart</h6>
<button cButton color="primary" variant="outline" size="sm"
(click)="addCondition(current_sequence.conditions_json)">
<i class="fa-solid fa-plus me-1"></i>Add Root Condition
</button>
</c-card-header>
<c-card-body class="bg-light p-4 overflow-auto bg-grid-pattern">
<div *ngIf="!current_sequence.conditions_json || current_sequence.conditions_json.length === 0"
class="text-center py-5 text-muted">
<i class="fa-solid fa-code-branch fa-3x mb-3 opacity-25"></i>
<h6>No conditions defined</h6>
<p class="small mb-0">The source snippet will execute unconditionally.<br />Add a condition below to
build branching logic based on the snippet's output.</p>
</div>
<div class="flow-tree"
*ngIf="current_sequence.conditions_json && current_sequence.conditions_json.length > 0">
<!-- Root Entry Node -->
<div class="flow-entry-node">
<div class="entry-bubble"><i class="fa-solid fa-play me-2"></i>Source Snippet Output</div>
<div class="flow-connector-down"></div>
</div>
<!-- Recursive Conditions Render -->
<ng-container
*ngTemplateOutlet="conditionNodeTemplate; context: { conditions: current_sequence.conditions_json }"></ng-container>
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer class="bg-white border-top-0 pt-0 pb-3 pe-4">
<button cButton color="secondary" variant="ghost" [cModalToggle]="EditSequenceModal.id">Cancel</button>
<button cButton color="primary" class="px-4" (click)="save_sequence()"><i class="fa-solid fa-save me-2"></i>Save
Sequence</button>
</c-modal-footer>
</c-modal>
<!-- RECURSIVE TEMPLATE FOR CONDITIONS -->
<ng-template #conditionNodeTemplate let-conditions="conditions">
<div class="condition-group" *ngIf="conditions && conditions.length > 0">
<div class="condition-branch" *ngFor="let cond of conditions; let i = index">
<!-- Condition Card -->
<div class="node-card condition-node">
<div
class="node-header bg-warning bg-opacity-10 text-warning d-flex justify-content-between align-items-center">
<span><i class="fa-solid fa-code-compare me-2"></i>Condition</span>
<button class="btn btn-link text-danger p-0 m-0" (click)="removeNode(conditions, i)"><i
class="fa-solid fa-times"></i></button>
</div>
<div class="node-body">
<c-row class="g-2 align-items-center">
<c-col xs="12" md="12" class="mb-2">
<select class="form-select form-select-sm" [(ngModel)]="cond.type">
<option value="contains">Contains Content</option>
<option value="not_contain">Does Not Contain</option>
</select>
</c-col>
<c-col xs="12" md="12">
<div class="input-group input-group-sm">
<div class="input-group-text bg-white border-end-0 pe-1">
<input class="form-check-input mt-0" type="checkbox" [(ngModel)]="cond.is_regex"
title="Use Regex">
<small class="ms-1 text-muted" title="Use Regex">Regex</small>
</div>
<input type="text" class="form-control border-start-0 ps-1" [(ngModel)]="cond.pattern"
placeholder="Match string or pattern...">
</div>
</c-col>
</c-row>
<div class="node-footer mt-2 pt-2 border-top text-center">
<button class="btn btn-sm btn-outline-primary py-0" style="font-size: 0.75rem"
(click)="addAction(cond)">
<i class="fa-solid fa-plus me-1"></i>Add Action
</button>
</div>
</div>
</div>
<!-- Flow Connector from Condition to Actions -->
<div class="flow-connector-down" *ngIf="cond.actions && cond.actions.length > 0"></div>
<!-- Render Actions under this condition -->
<div class="action-group" *ngIf="cond.actions && cond.actions.length > 0">
<div class="action-branch" *ngFor="let act of cond.actions; let j = index">
<div class="node-card action-node">
<div
class="node-header bg-success bg-opacity-10 text-success d-flex justify-content-between align-items-center">
<span><i class="fa-solid fa-bolt me-2"></i>Action</span>
<button class="btn btn-link text-danger p-0 m-0" (click)="removeNode(cond.actions, j)"><i
class="fa-solid fa-times"></i></button>
</div>
<div class="node-body">
<select class="form-select form-select-sm mb-2" [(ngModel)]="act.action_type">
<option value="alert_set">Trigger Alert</option>
<option value="alert_clear">Clear Alert</option>
<option value="execute_snippet">Execute Another Snippet</option>
</select>
<!-- Alert Selection -->
<div *ngIf="act.action_type === 'alert_set' || act.action_type === 'alert_clear'">
<select class="form-select form-select-sm" [(ngModel)]="act.alert_id">
<option [ngValue]="null">Select an Alert Definition...</option>
<option *ngFor="let a of Alerts" [value]="a.id">{{a.name}} ({{a.level}})</option>
</select>
</div>
<!-- Snippet Selection -->
<div *ngIf="act.action_type === 'execute_snippet'">
<select class="form-select form-select-sm" [(ngModel)]="act.snippet_id">
<option [ngValue]="null">Select a Snippet to Run...</option>
<option *ngFor="let s of Snippets" [value]="s.id">{{s.name}}</option>
</select>
<!-- Recursive Conditions for nested Snippet -->
<div class="mt-2 pt-2 border-top text-center">
<button class="btn btn-sm btn-outline-warning py-0" style="font-size: 0.75rem"
(click)="addCondition(act.conditions || (act.conditions = []))">
<i class="fa-solid fa-code-compare me-1"></i>Add Nested Condition
</button>
</div>
</div>
</div>
</div>
<!-- Recursive render of nested conditions from Snippet Action -->
<div *ngIf="act.action_type === 'execute_snippet' && act.conditions && act.conditions.length > 0">
<div class="flow-connector-down"></div>
<ng-container
*ngTemplateOutlet="conditionNodeTemplate; context: { conditions: act.conditions }"></ng-container>
</div>
</div> <!-- end action-branch loop -->
</div> <!-- end action-group -->
</div> <!-- end condition-branch loop -->
</div> <!-- end condition-group -->
</ng-template>
<!-- Manage Alerts Modal -->
<c-modal #ManageAlertsModal backdrop="static" size="lg" [(visible)]="ManageAlertsModalVisible" id="ManageAlertsModal">
<c-modal-header class="bg-light">
<h5 cModalTitle><i class="fa-solid fa-bell me-2"></i>Manage Alert Definitions</h5>
<button [cModalToggle]="ManageAlertsModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4 bg-light">
<c-row>
<!-- Form Column -->
<c-col xs="12" md="4" class="mb-4">
<c-card class="shadow-sm border-0">
<c-card-header class="bg-white">
<h6 class="mb-0">{{editingAlert.id === 0 ? 'Create Alert' : 'Edit Alert'}}</h6>
</c-card-header>
<c-card-body>
<div class="mb-3">
<label class="form-label small text-muted">Alert Name</label>
<input class="form-control" [(ngModel)]="editingAlert.name" placeholder="e.g. CPU High">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Severity Level</label>
<select class="form-select" [(ngModel)]="editingAlert.level">
<option value="Warning">Warning</option>
<option value="Critical">Critical</option>
<option value="Error">Error</option>
</select>
</div>
<button cButton color="primary" class="w-100" (click)="saveAlert()">
<i class="fa-solid fa-save me-1"></i>Save Alert
</button>
<button *ngIf="editingAlert.id !== 0" cButton color="secondary" variant="ghost"
class="w-100 mt-2" (click)="editingAlert = { id: 0, name: '', level: 'info' }">Cancel
Edit</button>
</c-card-body>
</c-card>
</c-col>
<!-- List Column -->
<c-col xs="12" md="8">
<c-card class="shadow-sm border-0">
<c-card-header class="bg-white">
<h6 class="mb-0">Existing Alerts</h6>
</c-card-header>
<c-card-body class="p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th>Name</th>
<th>Level</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let alert of Alerts">
<td class="fw-semibold">{{alert.name}}</td>
<td>
<c-badge
[color]="alert.level === 'critical' ? 'danger' : (alert.level === 'warning' ? 'warning' : 'info')">
{{alert.level | uppercase}}
</c-badge>
</td>
<td class="text-end">
<button cButton color="info" size="sm" variant="ghost" class="me-1"
(click)="editAlert(alert)"><i class="fa-solid fa-edit"></i></button>
<button cButton color="danger" size="sm" variant="ghost"
(click)="deleteAlert(alert.id)"><i
class="fa-solid fa-trash"></i></button>
</td>
</tr>
<tr *ngIf="Alerts.length === 0">
<td colspan="3" class="text-center text-muted py-4">No alerts defined.</td>
</tr>
</tbody>
</table>
</div>
</c-card-body>
</c-card>
</c-col>
</c-row>
</c-modal-body>
</c-modal>
<!-- Execution History Modal -->
<c-modal #HistoryModal backdrop="static" size="xl" [(visible)]="HistoryModalVisible" id="HistoryModal">
<c-modal-header class="bg-light">
<h5 cModalTitle><i class="fa-solid fa-clock-rotate-left me-2"></i>Execution History: {{viewing_sequence_name}}
</h5>
<button [cModalToggle]="HistoryModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4 bg-light">
<c-card class="shadow-sm border-0">
<c-card-body class="p-0">
<div *ngIf="sequence_history.length > 0">
<div *ngIf="sequence_history.length > 0">
<div *ngFor="let run of sequence_history; let i = index" class="mb-3">
<c-card class="border-0 shadow-sm overflow-hidden">
<c-card-header (click)="run.visible = !run.visible" style="cursor: pointer;"
class="d-flex justify-content-between align-items-center bg-white border-0 py-3">
<span>
<i class="fa-solid"
[ngClass]="run.visible ? 'fa-chevron-down' : 'fa-chevron-right'"
class="me-3 text-muted"></i>
<i class="fa-solid fa-calendar-check me-2 text-primary"></i>
<strong>{{run.created}}</strong>
<small class="text-muted ms-3">Exec ID: {{run.exec_id | slice:0:8}}...</small>
</span>
<div class="d-flex align-items-center">
<c-badge
[color]="run.successCount === run.totalCount ? 'success' : (run.successCount === 0 ? 'danger' : 'warning')"
class="px-3 py-2 text-white">
{{run.successCount}}/{{run.totalCount}} Success
</c-badge>
</div>
</c-card-header>
<div [visible]="run.visible" cCollapse class="border-top">
<c-card-body class="p-0 border-top">
<p-table [value]="run.devices" [paginator]="true" [rows]="5"
[showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm"
class="border-0">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="device_id">Device <p-sortIcon
field="device_id"></p-sortIcon></th>
<th pSortableColumn="status" class="text-center"
style="width: 150px">Status <p-sortIcon
field="status"></p-sortIcon></th>
<th style="width: 150px" class="text-end">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-dev>
<tr>
<td>
<div class="fw-semibold ms-2">
<i class="fa-solid fa-microchip me-2 opacity-50"></i>Device
#{{dev.device_id}}
</div>
</td>
<td class="text-center">
<c-badge
[color]="dev.status === 'success' ? 'success' : 'danger'"
class="text-white px-2">
{{dev.status | uppercase}}
</c-badge>
</td>
<td class="text-end">
<button cButton color="dark" size="sm" variant="outline"
pTooltip="Trace Log" (click)="showTrace(dev)">
<i class="fa-solid fa-terminal"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</c-card-body>
</div>
</c-card>
</div>
</div>
</div>
<div *ngIf="sequence_history.length === 0" class="text-center text-muted py-5">
<i class="fa-solid fa-inbox fa-3x mb-3 opacity-25"></i>
<h6>No History Found</h6>
<p class="small mb-0">This sequence hasn't been executed yet or history tracking is disabled.</p>
</div>
</c-card-body>
</c-card>
</c-modal-body>
</c-modal>
<!-- Trace Log Modal (Terminal Style) -->
<c-modal #TraceModal backdrop="static" size="xl" [(visible)]="TraceModalVisible" id="TraceModal">
<c-modal-header class="bg-dark text-white">
<h5 cModalTitle><i class="fa-solid fa-terminal me-2"></i>Execution Trace: Device #{{viewing_device_id}}</h5>
<button (click)="TraceModalVisible=false" cButtonClose white></button>
</c-modal-header>
<c-modal-body class="p-0 bg-dark">
<div class="nav nav-tabs border-secondary px-3 pt-2">
<div class="nav-item">
<a [cTabContent]="tabContent" [active]="true" [tabPaneIdx]="0"
class="nav-link text-light border-secondary cursor-pointer">
<i class="fa-solid fa-code me-2"></i>SSH Output
</a>
</div>
<div class="nav-item">
<a [cTabContent]="tabContent" [active]="false" [tabPaneIdx]="1"
class="nav-link text-light border-secondary cursor-pointer">
<i class="fa-solid fa-list-check me-2"></i>Evaluation Details
</a>
</div>
</div>
<c-tab-content #tabContent="cTabContent" class="p-3">
<c-tab-pane class="p-0">
<div class="terminal-container">
<div class="terminal-content">
<div highlight-js lang="routeros" [options]="{}">{{
current_trace.parsedLog?.ssh_output || current_trace.task_log }}</div>
</div>
</div>
</c-tab-pane>
<c-tab-pane class="p-3 text-light">
<div class="evaluation-tree" *ngIf="current_trace.parsedLog?.evaluation">
<!-- Structured JSON Evaluation -->
<div *ngIf="isObject(current_trace.parsedLog.evaluation); else flatEval">
<ng-container
*ngTemplateOutlet="evalNodeTemplate; context: { node: current_trace.parsedLog.evaluation }"></ng-container>
</div>
<!-- Legacy String Evaluation -->
<ng-template #flatEval>
<pre class="text-info bg-transparent border-0 p-0 m-0"
style="white-space: pre-wrap;">{{current_trace.parsedLog.evaluation}}</pre>
</ng-template>
</div>
<div *ngIf="!current_trace.parsedLog?.evaluation" class="text-center py-4 text-muted">
No evaluation details found for this run.
</div>
</c-tab-pane>
</c-tab-content>
</c-modal-body>
<c-modal-footer class="bg-dark border-secondary">
<button cButton color="secondary" (click)="TraceModalVisible=false">Close Trace</button>
</c-modal-footer>
</c-modal>
<!-- RECURSIVE TEMPLATE FOR EVALUATION LOGS -->
<ng-template #evalNodeTemplate let-node="node">
<div class="eval-node ms-3 border-start border-secondary ps-3 mb-3">
<div *ngFor="let rule of (isArray(node) ? node : [node])" class="mb-2">
<div class="d-flex align-items-center mb-1">
<i class="fa-solid fa-check-circle text-success me-2" *ngIf="rule.matched"></i>
<i class="fa-regular fa-circle text-muted me-2" *ngIf="!rule.matched"></i>
<span [class.text-white]="rule.matched" [class.text-muted]="!rule.matched">
Rule <strong>[{{rule.type}}]</strong>: '{{rule.pattern}}' &rarr;
<span [class.text-success]="rule.matched">{{rule.matched ? 'MATCHED' : 'NO MATCH'}}</span>
</span>
</div>
<div *ngIf="rule.matched && rule.actions?.length > 0" class="ms-4 my-2 p-2 bg-opacity-10 bg-info rounded">
<div *ngFor="let act of rule.actions" class="mb-1 small">
<i class="fa-solid fa-arrow-right text-info me-2"></i>
<strong>ACTION:</strong> {{act.action_type}}
<span *ngIf="act.alert_id" class="text-warning ms-1">(Alert ID: {{act.alert_id}})</span>
<span *ngIf="act.snippet_id" class="text-primary ms-1">(Snippet: {{act.snippet_id}})</span>
<!-- Nested Evaluation if another snippet was run -->
<div *ngIf="act.nested_eval" class="mt-2">
<ng-container
*ngTemplateOutlet="evalNodeTemplate; context: { node: act.nested_eval }"></ng-container>
</div>
</div>
</div>
</div>
</div>
</ng-template>
<!-- MANUAL EXECUTION MODAL -->
<c-modal #ExecSequenceModal backdrop="static" size="xl" [(visible)]="ExecSequenceModalVisible" id="ExecSequenceModal">
<c-modal-header>
<h5 cModalTitle>Exec Sequence</h5>
<button [cModalToggle]="ExecSequenceModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingExecName" placeholder="current_sequence['name']"
[(ngModel)]="current_sequence['name']" disabled="true" />
<label cLabel for="floatingExecName">Sequence Name</label>
</div>
<div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingExecDesc" placeholder="Description"
[(ngModel)]="current_sequence['description']" />
<label cLabel for="floatingExecDesc">Description</label>
</div>
<c-input-group class="mb-3">
<label cInputGroupText for="inputGroupSelectSeq">
Member type
</label>
<select cSelect id="inputGroupSelectSeq" (change)="form_changed()"
[(ngModel)]="current_sequence['selection_type']">
<option value="devices">Devices</option>
<option value="groups">Groups</option>
</select>
</c-input-group>
<h5>Members :</h5>
<p-table #dtMembers [value]="SelectedMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name">Name <p-sortIcon field="name"></p-sortIcon></th>
<th *ngIf="current_sequence['selection_type']=='devices'" pSortableColumn="mac">MAC <p-sortIcon
field="mac"></p-sortIcon></th>
<th style="width: 100px" class="text-center">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>{{item.name}}</td>
<td *ngIf="current_sequence['selection_type']=='devices'">{{item.mac}}</td>
<td class="text-center">
<button cButton color="danger" size="sm" (click)="remove_member(item)" pTooltip="Remove Member">
<i class="fa-regular fa-trash-can"></i>
</button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="current_sequence['selection_type']=='devices' ? 3 : 2" class="text-center p-2">
No members added.</td>
</tr>
</ng-template>
</p-table>
<hr />
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
</c-modal-body>
<c-modal-footer>
<button (click)="submit_exec()" cButton color="primary">Execute</button>
<button [cModalToggle]="ExecSequenceModal.id" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>
<!-- MEMBER SELECTION MODAL -->
<c-modal #NewMemberModal backdrop="static" size="lg" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<c-modal-header>
<h5 cModalTitle>Editing Group </h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<h5>Group Members :</h5>
<div class="mb-3 d-flex justify-content-end w-100">
<span class="p-input-icon-left table-search-input">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search members..."
(input)="applyFilterNewMembers($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" styleClass="p-datatable-sm" [(selection)]="SelectedNewMemberRows"
(selectionChange)="onSelectedRowsNewMembers($event)" [globalFilterFields]="['name', 'ip', 'mac']">
<ng-template pTemplate="header">
<tr>
<th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
<th pSortableColumn="name" pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>Member Name</span>
<p-sortIcon field="name"></p-sortIcon>
</div>
</th>
<th *ngIf="current_sequence['selection_type']=='devices'" pSortableColumn="ip" pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>IP Address</span>
<p-sortIcon field="ip"></p-sortIcon>
</div>
</th>
<th *ngIf="current_sequence['selection_type']=='devices'" pSortableColumn="mac"
pResizableColumn>
<div class="d-flex justify-content-between align-items-center">
<span>MAC Address</span>
<p-sortIcon field="mac"></p-sortIcon>
</div>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
<td>{{item.name}}</td>
<td *ngIf="current_sequence['selection_type']=='devices'">{{item.ip}}</td>
<td *ngIf="current_sequence['selection_type']=='devices'">{{item.mac}}</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="current_sequence['selection_type']=='devices' ? 4 : 2"
class="text-center p-4">No available members found.</td>
</tr>
</ng-template>
</p-table>
<br />
</c-input-group>
<hr />
</c-modal-body>
<c-modal-footer>
<button (click)="add_new_members()" cButton color="primary">Add Selected</button>
<button (click)="NewMemberModalVisible=false" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>

View file

@ -0,0 +1,257 @@
/* Flowchart Builder Styling */
.bg-grid-pattern {
background-image: linear-gradient(to right, #e8ecef 1px, transparent 1px), linear-gradient(to bottom, #e8ecef 1px, transparent 1px);
background-size: 20px 20px;
}
.flow-tree {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0;
}
.flow-entry-node {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 0;
}
.entry-bubble {
background: #321fdb;
color: white;
padding: 0.5rem 1.5rem;
border-radius: 50px;
font-weight: 600;
box-shadow: 0 4px 6px rgba(50, 31, 219, 0.2);
z-index: 2;
}
/* Connectors */
.flow-connector-down {
width: 2px;
height: 30px;
background-color: #9da5b1;
position: relative;
z-index: 1;
}
/* Groups and Branches */
/* Groups and Branches */
.condition-group,
.action-group {
display: flex;
justify-content: center;
gap: 0;
position: relative;
/* No padding-top here, used on branches instead */
flex-wrap: nowrap;
min-width: max-content;
}
/* Horizontal connectors refined */
.condition-branch::after,
.action-branch::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background-color: #9da5b1;
z-index: 1;
}
.condition-branch:first-child::after,
.action-branch:first-child::after {
left: 50%;
}
.condition-branch:last-child::after,
.action-branch:last-child::after {
right: 50%;
}
.condition-branch:only-child::after,
.action-branch:only-child::after {
display: none;
}
.condition-branch,
.action-branch {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 30px 1rem 0 1rem; /* 30px top padding for connectors */
flex: 1 1 0px; /* Equal width branches */
min-width: 240px; /* Minimum width to keep content readable */
max-width: 400px; /* Reasonable expansion limit */
}
/* Horizontal connectors spanning between centers of nodes */
.condition-branch::after,
.action-branch::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background-color: #9da5b1;
z-index: 1;
}
.condition-branch:first-child::after,
.action-branch:first-child::after {
left: 50%;
}
.condition-branch:last-child::after,
.action-branch:last-child::after {
right: 50%;
}
.condition-branch:only-child::after,
.action-branch:only-child::after {
display: none;
}
/* Vertical connector from horizontal bar down to card */
.condition-branch::before,
.action-branch::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 30px;
background-color: #9da5b1;
z-index: 1;
}
/* Connector from parent node down to the group's horizontal bar */
.flow-connector-down {
width: 2px;
height: 30px;
background-color: #9da5b1;
margin: 0 auto;
position: relative;
z-index: 1;
}
/* Cards with Dynamic Widths based on branch width */
.node-card {
width: 100%; /* Fill the branch */
background: #fff;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
border: 1px solid #d8dbe0;
overflow: hidden;
position: relative;
z-index: 2;
transition: all 0.2s ease;
}
.node-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.node-header {
padding: 0.5rem 1rem;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.node-body {
padding: 1rem;
}
.condition-node {
border-top: 4px solid #f9b115;
}
.action-node {
border-top: 4px solid #2eb85c;
}
/* Modal Wide Override */
::ng-deep .builder-modal .modal-xl {
max-width: 95vw !important;
}
.flowchart-container {
min-height: 60vh;
}
.bg-dark {
background-color: #1a1d21 !important;
}
.terminal-container {
background-color: #0d1117;
border-radius: 6px;
padding: 1rem;
max-height: 60vh;
overflow-y: auto;
border: 1px solid #30363d;
.terminal-content {
background-color: transparent;
::ng-deep pre {
display: block !important;
white-space: pre-wrap !important;
word-break: break-all !important;
min-height: 50vh !important;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-size: 0.9rem !important;
color: #e6edf3 !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
}
::ng-deep .hljs {
background: transparent !important;
padding: 0 !important;
font-family: inherit !important;
font-size: inherit !important;
}
}
}
.cursor-pointer {
cursor: pointer;
}
.hover-shadow:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
::ng-deep .nav-tabs .nav-link {
cursor: pointer;
}
.border-secondary {
border-color: #30363d !important;
}
::ng-deep .nav-tabs .nav-link.active {
background-color: #0d1117 !important;
border-color: #30363d #30363d transparent !important;
color: #fff !important;
}
::ng-deep .nav-tabs .nav-link {
border: 1px solid transparent;
}
::ng-deep .nav-tabs {
border-bottom: 1px solid #30363d;
}

View file

@ -0,0 +1,413 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { dataProvider } from "../../providers/mikrowizard/data";
import { ToastComponent } from '@coreui/angular';
import { NgxSuperSelectOptions } from "ngx-super-select";
import { Table } from 'primeng/table';
@Component({
selector: 'app-sequences',
templateUrl: './sequences.component.html',
styleUrls: ['./sequences.component.scss']
})
export class SequencesComponent implements OnInit {
public sequences: any[] = [];
public source: any[] = [];
@ViewChild('dt') table!: Table;
@ViewChild('dtMembers') tableMembers!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
applyFilterMembers($event: any, stringVal: string) {
this.tableMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
applyFilterNewMembers($event: any, stringVal: string) {
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
public EditSequenceModalVisible: boolean = false;
public current_sequence: any = {
id: 0,
name: '',
source_snippet_id: null,
store_all_history: false,
is_active: true,
conditions_json: []
};
public ManageAlertsModalVisible: boolean = false;
public editingAlert: any = { id: 0, name: '', level: 'Info' };
public Snippets: any = [];
public Alerts: any = [];
public ModalAction: string = "add";
public ExecSequenceModalVisible: boolean = false;
public SelectedSequence: any = {};
public SelectedMembers: any[] = [];
public SelectedTaskItems: any[] = [];
public availbleMembers: any[] = [];
public NewMemberModalVisible: boolean = false;
public SelectedNewMemberRows: any[] = [];
public NewMemberRows: any[] = [];
public loading: boolean = true;
// super select options
snippetOptions: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Select Snippet",
searchEnabled: true,
};
alertOptions: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Select Alert",
searchEnabled: true,
};
constructor(public MikroWizardRPC: dataProvider) { }
ngOnInit(): void {
this.loadSequences();
this.loadSnippets();
this.loadAlerts();
}
loadSequences() {
this.MikroWizardRPC.get_sequences().then(
(res: any) => {
if (res) {
this.source = res.map((seq: any) => {
if (typeof seq.conditions_json === 'string') {
try {
seq.conditions_json = JSON.parse(seq.conditions_json);
} catch (e) {
seq.conditions_json = [];
}
}
if (!seq.conditions_json) seq.conditions_json = [];
return seq;
});
}
},
(error: any) => {
// Handle error visually if needed
}
);
}
loadSnippets(): void {
this.MikroWizardRPC.get_snippets("", "", "", 0, 1000, false).then((res: any) => {
this.Snippets = res.map((x: any) => ({ id: x.id, name: x.name }));
});
}
loadAlerts(): void {
this.MikroWizardRPC.get_alerts().then((res: any) => {
this.Alerts = res.map((x: any) => ({ id: x.id, name: x.name, level: x.level }));
});
}
addCondition(targetArray: any[]) {
targetArray.push({
type: 'contains',
is_regex: false,
pattern: '',
actions: []
});
}
addAction(condition: any) {
if (!condition.actions) condition.actions = [];
condition.actions.push({
action_type: 'alert_set',
alert_id: null,
snippet_id: null,
conditions: []
});
}
removeNode(array: any[], index: number) {
if (array && array.length > index) {
array.splice(index, 1);
}
}
Edit_Sequence(item: any, mode: string) {
if (mode === 'add') {
this.current_sequence = {
id: 0,
name: '',
source_snippet_id: null,
store_all_history: false,
is_active: true,
conditions_json: []
};
this.ModalAction = 'add';
} else {
// deep copy
this.current_sequence = JSON.parse(JSON.stringify(item));
if (!this.current_sequence.conditions_json) this.current_sequence.conditions_json = [];
this.ModalAction = 'edit';
}
this.EditSequenceModalVisible = true;
}
save_sequence() {
let payload = { ...this.current_sequence };
if (payload.conditions_json && typeof payload.conditions_json !== 'string') {
payload.conditions_json = JSON.stringify(payload.conditions_json);
}
this.MikroWizardRPC.save_sequence(payload).then(() => {
this.EditSequenceModalVisible = false;
this.loadSequences();
});
}
onSelectSourceSnippet($event: any) {
this.current_sequence.source_snippet_id = $event;
}
// --- Alert Management ---
openManageAlerts() {
this.ManageAlertsModalVisible = true;
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
}
editAlert(item: any) {
this.editingAlert = { ...item };
}
saveAlert() {
this.MikroWizardRPC.save_alert(this.editingAlert).then(() => {
this.loadAlerts();
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
});
}
deleteAlert(id: number) {
if (confirm("Are you sure you want to delete this alert?")) {
this.MikroWizardRPC.delete_alert(id).then(() => {
this.loadAlerts();
});
}
}
confirm_delete(item: any, confirm: boolean) {
if (!confirm) {
if (window.confirm("Are you sure you want to delete sequence: " + item.name + "?")) {
this.MikroWizardRPC.delete_sequence(item.id).then(() => {
this.loadSequences();
});
}
}
}
public HistoryModalVisible: boolean = false;
public sequence_history: any[] = [];
public viewing_sequence_name: string = '';
public TraceModalVisible: boolean = false;
public current_trace: any = {};
public viewing_device_id: number = 0;
showTrace(dev: any) {
this.current_trace = dev;
this.viewing_device_id = dev.device_id;
this.TraceModalVisible = true;
}
show_history(item: any) {
this.viewing_sequence_name = item.name;
this.sequence_history = [];
this.HistoryModalVisible = true;
this.MikroWizardRPC.get_sequence_history(item.id).then((res: any) => {
// Process the nested history response
this.sequence_history = res.map((run: any) => {
const total = run.devices.length;
const success = run.devices.filter((d: any) => d.status === 'success').length;
return {
...run,
successCount: success,
totalCount: total,
visible: false,
devices: run.devices.map((dev: any) => ({
...dev,
parsedLog: this.parseLog(dev.task_log)
}))
};
});
});
}
parseLog(logJson: any) {
if (!logJson) return { ssh_output: '', evaluation: '' };
let processed = logJson;
// Normalize any escaped newlines if it's a string
if (typeof logJson === 'string') {
processed = logJson.split('\\n').join('\n').replace(/\r\n/g, '\n');
}
// If it's already an object, use it directly (but normalize ssh_output)
if (typeof processed === 'object' && processed !== null) {
if (processed.ssh_output) {
processed.ssh_output = processed.ssh_output.split('\\n').join('\n').replace(/\r\n/g, '\n');
}
return processed;
}
// Try to parse as JSON first (new format)
try {
if (typeof processed === 'string' && processed.trim().startsWith('{')) {
const parsed = JSON.parse(processed);
if (parsed.ssh_output) {
parsed.ssh_output = parsed.ssh_output.split('\\n').join('\n').replace(/\r\n/g, '\n');
}
return parsed;
}
} catch (e) {
// Not JSON, continue to legacy parsing
}
// Legacy Interleaved Log parsing
if (typeof processed !== 'string') return { ssh_output: processed, evaluation: '' };
const lines = processed.split('\n');
let outputSections: string[] = [];
let evalSections: string[] = [];
let currentSection: string[] = [];
let isCurrentOutput = true;
for (let line of lines) {
const lowerLine = line.toLowerCase();
const startsNewOutput = lowerLine.includes('output (') || (lowerLine.includes('ssh output') && !lowerLine.includes('failed'));
const startsNewEval = lowerLine.includes('evaluation');
if (startsNewOutput || startsNewEval) {
// Save previous section
if (currentSection.length > 0) {
const block = currentSection.join('\n').trim();
if (isCurrentOutput) outputSections.push(block);
else evalSections.push(block);
}
// Start new section
currentSection = [line];
isCurrentOutput = startsNewOutput;
} else {
currentSection.push(line);
}
}
// Final section
if (currentSection.length > 0) {
const block = currentSection.join('\n').trim();
if (isCurrentOutput) outputSections.push(block);
else evalSections.push(block);
}
return {
ssh_output: outputSections.join('\n\n').trim(),
evaluation: evalSections.join('\n\n').trim()
};
}
// --- Manual Execution ---
Run_Sequence(item: any) {
this.SelectedSequence = item;
this.current_sequence = { ...item };
this.current_sequence["selection_type"] = "devices";
this.SelectedMembers = [];
this.SelectedTaskItems = [];
this.ExecSequenceModalVisible = true;
}
form_changed() {
this.SelectedMembers = [];
this.SelectedTaskItems = [];
}
remove_member(item: any) {
this.SelectedMembers = this.SelectedMembers.filter(
(x: any) => x.id != item.id
);
this.SelectedTaskItems = this.SelectedMembers.map((x: any) => {
return x.id;
});
}
show_new_member_form() {
this.NewMemberModalVisible = true;
this.availbleMembers = [];
this.SelectedNewMemberRows = [];
this.NewMemberRows = [];
var data = {
group_id: false,
search: false,
page: false,
size: 10000,
};
if (this.current_sequence["selection_type"] == "devices")
this.MikroWizardRPC.get_dev_list(data).then((res: any) => {
this.availbleMembers = res.filter(
(x: any) => !this.SelectedTaskItems.includes(x.id)
);
});
else
this.MikroWizardRPC.get_devgroup_list().then((res: any) => {
this.availbleMembers = res.filter(
(x: any) => !this.SelectedTaskItems.includes(x.id)
);
});
}
onSelectedRowsNewMembers(rows: any): void {
this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: any) => m.source);
}
isObject(val: any): boolean {
return typeof val === 'object' && val !== null && !Array.isArray(val);
}
isArray(val: any): boolean {
return Array.isArray(val);
}
add_new_members() {
this.SelectedMembers = [
...new Set(this.SelectedMembers.concat(this.SelectedNewMemberRows)),
];
this.SelectedTaskItems = this.SelectedMembers.map((x: any) => {
return x.id;
});
this.NewMemberModalVisible = false;
}
submit_exec() {
const payload = {
sequence_id: this.SelectedSequence.id,
selection_type: this.current_sequence.selection_type,
members: this.SelectedTaskItems
};
this.MikroWizardRPC.exec_sequence(payload).then((res: any) => {
this.ExecSequenceModalVisible = false;
// Optionally show success toast or refresh history
});
}
}

View file

@ -0,0 +1,56 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import {
ButtonGroupModule,
ButtonModule,
CardModule,
FormModule,
GridModule,
ToastModule,
ModalModule,
BadgeModule,
AccordionModule,
NavModule,
TabsModule,
AlertModule,
CollapseModule
} from "@coreui/angular";
import { HighlightJsModule } from 'ngx-highlight-js';
import { SequencesRoutingModule } from "./sequences-routing.module";
import { SequencesComponent } from "./sequences.component";
import { TableModule } from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { TooltipModule } from 'primeng/tooltip';
import { NgxSuperSelectModule } from "ngx-super-select";
import { SharedModule } from "../../shared/shared.module";
@NgModule({
imports: [
SequencesRoutingModule,
CardModule,
CommonModule,
GridModule,
FormModule,
ButtonModule,
ButtonGroupModule,
TableModule,
InputTextModule,
TooltipModule,
ModalModule,
ToastModule,
FormsModule,
BadgeModule,
NgxSuperSelectModule,
AccordionModule,
NavModule,
TabsModule,
AlertModule,
HighlightJsModule,
CollapseModule,
SharedModule
],
declarations: [SequencesComponent],
})
export class SequencesModule { }

File diff suppressed because it is too large Load diff

View file

@ -7,12 +7,11 @@
} }
.mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted { .mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted {
display: none!important; display: none !important;
} }
/* Settings Container */ /* Settings Container */
.settings-container { .settings-container {
max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
@ -24,7 +23,7 @@
} }
.settings-header { .settings-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); // background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #dee2e6;
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
} }
@ -205,7 +204,8 @@
/* Form Controls */ /* Form Controls */
.form-control, .form-select { .form-control,
.form-select {
border-radius: 6px; border-radius: 6px;
border: 1px solid #ced4da; border: 1px solid #ced4da;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@ -213,7 +213,8 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.form-control:focus, .form-select:focus { .form-control:focus,
.form-select:focus {
border-color: #0d6efd; border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
} }
@ -250,6 +251,7 @@
/* Mobile Responsive for Informative Elements */ /* Mobile Responsive for Informative Elements */
@media (max-width: 768px) { @media (max-width: 768px) {
.info-banner, .info-banner,
.warning-banner, .warning-banner,
.success-banner { .success-banner {
@ -280,6 +282,7 @@
} }
@media (max-width: 576px) { @media (max-width: 576px) {
.info-banner, .info-banner,
.warning-banner, .warning-banner,
.success-banner { .success-banner {
@ -421,8 +424,15 @@
/* Animation for dropdown */ /* Animation for dropdown */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.search-dropdown { .search-dropdown {
@ -619,15 +629,3 @@
color: #721c24; color: #721c24;
border: 1px solid #f5c6cb; 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;
}
}

View file

@ -4,21 +4,12 @@ import {
QueryList, QueryList,
ViewChildren, ViewChildren,
ViewEncapsulation, ViewEncapsulation,
ViewChild
} from "@angular/core"; } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { ToasterComponent } from "@coreui/angular"; import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component"; import { AppToastComponent } from "../toast-simple/toast.component";
import { TimeZones } from "./timezones-data"; import { TimeZones } from "./timezones-data";
@ -30,15 +21,17 @@ import { TimeZones } from "./timezones-data";
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public ispro:boolean=false; public ispro:boolean=false;
public filterText: string; public filterText: string = '';
public filters: any = {}; public filters: any = {};
public firms: any = {}; public firms: any = {};
public firmtodownload: any = {}; public firmtodownload: any = {};
public activeTab: string = 'firmware'; public activeTab: string = 'firmware';
@ViewChild('dt') dt!: Table;
// Search functionality properties // Search functionality properties
public firmwareSearch: string = ''; public firmwareSearch: string = '';
public showFirmwareDropdown: boolean = false; public showFirmwareDropdown: boolean = false;
@ -81,7 +74,6 @@ export class SettingsComponent implements OnInit {
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public SysConfigloading: boolean = true; public SysConfigloading: boolean = true;
@ -104,37 +96,9 @@ export class SettingsComponent implements OnInit {
closeButton: true, closeButton: true,
}; };
public sorting = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.dt.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
multiSorting: true, }
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 5,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
public timezones = this.TimeZones.timezones; public timezones = this.TimeZones.timezones;
ngOnInit(): void { ngOnInit(): void {
@ -208,9 +172,9 @@ export class SettingsComponent implements OnInit {
}); });
} }
onSelectedRows(rows: Array<GuiSelectedRow>): void { onSelectedRows(rows: any[]): void {
this.rows = rows; this.rows = rows;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id); this.Selectedrows = rows.map((m: any) => m.id);
} }
show_toast(title: string, body: string, color: string) { show_toast(title: string, body: string, color: string) {

View file

@ -15,7 +15,9 @@ import {
} from "@coreui/angular"; } from "@coreui/angular";
import { SettingsRoutingModule } from "./settings-routing.module"; import { SettingsRoutingModule } from "./settings-routing.module";
import { SettingsComponent } from "./settings.component"; import { SettingsComponent } from "./settings.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
@ -29,7 +31,9 @@ import { FormsModule } from "@angular/forms";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
SpinnerModule, SpinnerModule,
ToastModule, ToastModule,
ModalModule, ModalModule,

View file

@ -8,46 +8,71 @@
</c-col> </c-col>
<c-col xs [lg]="9"> <c-col xs [lg]="9">
<h6 style="text-align: right;"> <h6 style="text-align: right;">
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Snippet('','showadd')" <button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Snippet('','showadd')"
style="color: #fff;"><i class="fa-solid fa-plus"></i> </button> style="color: #fff;"><i class="fa-solid fa-plus"></i> </button>
</h6> </h6>
</c-col> </c-col>
</c-row> </c-row>
</c-card-header> </c-card-header>
<c-card-body> <c-card-body>
<gui-grid [source]="source" [searching]="searching" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true> [(selection)]="Selectedrows" [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true"
<gui-grid-column header="Name" field="name"> columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
<ng-template let-value="item.name" let-item="item" let-index="index"> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" /> [globalFilterFields]="['name','description']">
<i *ngIf="item.status=='updated'" style="color: green;margin: 5px;" class="fa-solid fa-check"></i> <ng-template pTemplate="header">
<i *ngIf="item.status=='failed'" style="color: red;margin: 5px;" class="fa-solid fa-x"></i> <tr>
{{value}} <th pSortableColumn="name" pResizableColumn>
</ng-template> <div class="justify-between">
</gui-grid-column> Name
<gui-grid-column header="Description" field="description"> <p-sortIcon field="name"></p-sortIcon>
<ng-template let-value="item.description" let-item="item" let-index="index"> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
{{value}} </div>
</ng-template> </th>
</gui-grid-column> <th pSortableColumn="description" pResizableColumn>
<gui-grid-column header="Created" field="created"> <div class="justify-between">
<ng-template let-value="item.created" let-item="item" let-index="index"> Description
<div>{{value}}</div> <p-sortIcon field="description"></p-sortIcon>
</ng-template> <p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
</gui-grid-column> </div>
<gui-grid-column header="Actions" field="action" align="center"> </th>
<ng-template let-value="item.id" let-item="item" let-index="index"> <th pSortableColumn="created" pResizableColumn>
<button cButton color="primary" size="sm" (click)="Edit_Snippet(item,'edit')" class=""><i <div class="justify-between">
class="fa-regular fa-pen-to-square mx-1"></i>Edit</button> Created
<button cButton color="warning" size="sm" (click)="Run_Snippet(item,'exec')" class="mx-1"><i <p-sortIcon field="created"></p-sortIcon>
class="fa-solid fa-bolt mx-1"></i>Execute</button> <p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
<button cButton color="info" size="sm" (click)="show_exec(item)" class="mx-1"><i </div>
class="fa-solid fa-bolt mx-1"></i>Data</button> </th>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)" class=""><i <th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
class="fa-regular fa-trash-can mx-1"></i>Delete</button> </tr>
</ng-template> </ng-template>
</gui-grid-column> <ng-template pTemplate="body" let-item>
</gui-grid> <tr>
<td>
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
<i *ngIf="item.status=='updated'" style="color: green;margin-right: 5px;" class="fa-solid fa-check"></i>
<i *ngIf="item.status=='failed'" style="color: red;margin-right: 5px;" class="fa-solid fa-x"></i>
{{item.name}}
</td>
<td>{{item.description}}</td>
<td class="small">{{item.created}}</td>
<td class="text-center">
<button cButton color="primary" size="sm" (click)="Edit_Snippet(item,'edit')" class="me-1">
<i class="fa-regular fa-pen-to-square me-1"></i>Edit
</button>
<button cButton color="warning" size="sm" (click)="Run_Snippet(item,'exec')" class="me-1">
<i class="fa-solid fa-bolt me-1"></i>Execute
</button>
<button cButton color="info" size="sm" (click)="show_exec(item)" class="me-1">
<i class="fa-solid fa-history me-1"></i>History
</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)">
<i class="fa-regular fa-trash-can me-1"></i>Delete
</button>
</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>
@ -63,7 +88,8 @@
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
<div [cFormFloating]="true" class="mb-3"> <div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingInput" placeholder="current_snippet['name']" [(ngModel)]="current_snippet['name']" disabled="true"/> <input cFormControl id="floatingInput" placeholder="current_snippet['name']" [(ngModel)]="current_snippet['name']"
disabled="true" />
<label cLabel for="floatingInput">Snipet Name</label> <label cLabel for="floatingInput">Snipet Name</label>
</div> </div>
@ -84,30 +110,36 @@
</c-input-group> </c-input-group>
<h5>Members :</h5> <h5>Members :</h5>
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting" <p-table #dtSelected [value]="SelectedMembers" [paginator]="true" [rows]="5" [resizableColumns]="true"
[infoPanel]="infoPanel" [rowSelection]="rowSelection" [autoResizeWidth]=true [paging]="paging"> columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
<gui-grid-column header="Name" field="name"> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped">
<ng-template let-value="item.name" let-item="item" let-index="index"> <ng-template pTemplate="header">
&nbsp; {{value}} </ng-template> <tr>
</gui-grid-column> <th pSortableColumn="name" pResizableColumn>Name <p-sortIcon field="name"></p-sortIcon></th>
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="MAC" field="mac"> <th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="mac" pResizableColumn>MAC
<ng-template let-value="item.mac" let-item="item" let-index="index"> <p-sortIcon field="mac"></p-sortIcon>
{{value}} </th>
</ng-template> <th style="width: 80px" class="text-center" pResizableColumn>Actions</th>
</gui-grid-column> </tr>
<gui-grid-column header="Actions" width="120" field="action"> </ng-template>
<ng-template let-value="item.id" let-item="item" let-index="index"> <ng-template pTemplate="body" let-member>
<button cButton color="danger" size="sm" (click)="remove_member(item)"><i <tr>
class="fa-regular fa-trash-can"></i></button> <td>{{member.name}}</td>
</ng-template> <td *ngIf="current_snippet['selection_type']=='devices'">{{member.mac}}</td>
</gui-grid-column> <td class="text-center">
</gui-grid> <button cButton color="danger" size="sm" (click)="remove_member(member)">
<i class="fa-regular fa-trash-can"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
<hr /> <hr />
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button> <button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
<button (click)="submit('exec')" cButton color="primary">Execute</button> <button (click)="submit('exec')" cButton color="primary">Execute</button>
<button [cModalToggle]="ExecSnipetModal.id" cButton color="secondary"> <button [cModalToggle]="ExecSnipetModal.id" cButton color="secondary">
Close Close
</button> </button>
@ -123,25 +155,48 @@
<c-modal-body> <c-modal-body>
<c-input-group class="mb-3"> <c-input-group class="mb-3">
<h5>Group Members :</h5> <h5>Group Members :</h5>
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching" <div class="mb-3 d-flex justify-content-end w-100">
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left">
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true <i class="pi pi-search"></i>
[paging]="paging"> <input type="text" pInputText placeholder="Search..." (input)="applyFilterNewMember($event, 'contains')"
<gui-grid-column header="Member Name" field="name"> class="form-control-sm" />
<ng-template let-value="item.name" let-item="item" let-index="index"> </span>
&nbsp; {{value}} </ng-template> </div>
</gui-grid-column> <p-table #dtNewMember [value]="availbleMembers" [paginator]="true" [rows]="10"
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="IP Address" field="ip"> [(selection)]="SelectedNewMemberRows" (selectionChange)="NewMemberRows = $event" [resizableColumns]="true"
<ng-template let-value="item.ip" let-item="item" let-index="index"> columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
{{value}} Class="p-datatable-sm p-datatable-gridlines p-datatable-striped" [globalFilterFields]="['name','ip','mac']">
</ng-template> <ng-template pTemplate="header">
</gui-grid-column> <tr>
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="MAC Address" field="mac"> <th style="width: 3rem" pResizableColumn>
<ng-template let-value="item.mac" let-item="item" let-index="index"> <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
{{value}} </th>
</ng-template> <th pSortableColumn="name" pResizableColumn>
</gui-grid-column> Name
</gui-grid> <p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</th>
<th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="ip" pResizableColumn>
IP
<p-sortIcon field="ip"></p-sortIcon>
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
</th>
<th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="mac" pResizableColumn>
MAC
<p-sortIcon field="mac"></p-sortIcon>
<p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-member>
<tr>
<td><p-tableCheckbox [value]="member"></p-tableCheckbox></td>
<td>{{member.name}}</td>
<td *ngIf="current_snippet['selection_type']=='devices'">{{member.ip}}</td>
<td *ngIf="current_snippet['selection_type']=='devices'">{{member.mac}}</td>
</tr>
</ng-template>
</p-table>
<br /> <br />
</c-input-group> </c-input-group>
<hr /> <hr />
@ -164,25 +219,44 @@
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
<c-input-group class="mb-3"> <c-input-group class="mb-3">
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching" <div class="mb-3 d-flex justify-content-end w-100">
[source]="ExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left">
[autoResizeWidth]=true <i class="pi pi-search"></i>
[paging]="paging"> <input type="text" pInputText placeholder="Search history..." (input)="applyFilterHistory($event, 'contains')"
<gui-grid-column header="Start time" field="start"> class="form-control-sm" />
<ng-template let-value="item['started']" let-item="item" let-index="index"> </span>
&nbsp; {{value}} </ng-template> </div>
</gui-grid-column> <p-table #dtHistory width="100%" [value]="ExecutedData" [paginator]="true" [rows]="10" [resizableColumns]="true"
<gui-grid-column header="End time" field="end"> columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
<ng-template let-value="item['ended']" let-item="item" let-index="index"> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
{{value}} [globalFilterFields]="['username','detail']">
</ng-template> <ng-template pTemplate="header">
</gui-grid-column> <tr>
<gui-grid-column header="info" field="mac" align="center"> <th pSortableColumn="started" pResizableColumn>
<ng-template let-value="item['result']" let-item="item" let-index="index"> Start time
<button (click)="exportToCsv(value)" color="primary" cButton>download</button> <p-sortIcon field="started"></p-sortIcon>
</ng-template> <p-columnFilter type="text" field="started" display="menu" class="ms-auto" />
</gui-grid-column> </th>
</gui-grid> <th pSortableColumn="ended" pResizableColumn>
End time
<p-sortIcon field="ended"></p-sortIcon>
<p-columnFilter type="text" field="ended" display="menu" class="ms-auto" />
</th>
<th style="width: 120px" class="text-center" pResizableColumn>Info</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td class="small">{{item.started}}</td>
<td class="small">{{item.ended}}</td>
<td class="text-center">
<button (click)="exportToCsv(item.result)" color="primary" pTooltip="Download CSV" size="sm" cButton>
<i class="fas fa-download"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
<br /> <br />
</c-input-group> </c-input-group>
<hr /> <hr />
@ -210,16 +284,19 @@
</c-input-group> </c-input-group>
<c-input-group class="mb-3"> <c-input-group class="mb-3">
<div [cFormFloating]="true" class="mb-3"> <div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingInput" placeholder="Snippet Description" [(ngModel)]="current_snippet['description']" /> <input cFormControl id="floatingInput" placeholder="Snippet Description"
[(ngModel)]="current_snippet['description']" />
<label cLabel for="floatingInput">Description</label> <label cLabel for="floatingInput">Description</label>
</div> </div>
</c-input-group> </c-input-group>
<c-input-group class="mb-3"> <c-input-group class="mb-3">
<div [cFormFloating]="true" class="mb-3"> <div [cFormFloating]="true" class="mb-3">
<textarea [style.height.px]="50 + (23 * lineNum)" <textarea [style.height.px]="50 + (23 * lineNum)" cFormControl (ngModelChange)="calcline($event)"
cFormControl (ngModelChange)="calcline($event)" id="floatingInput" placeholder="Snippet code" [(ngModel)]="current_snippet['content']" ></textarea> id="floatingInput" placeholder="Snippet code" [(ngModel)]="current_snippet['content']"></textarea>
<label cLabel for="floatingInput">Code</label> <label cLabel for="floatingInput">Code</label>
<div class="col-sm-12 c-d-block c-text-truncate">Note : In case of multiple IP addresses for the MikroWizard server, use<code style="padding: 0!important;">[mikrowizard]</code> instead of the MikroWizard server IP.</div> <div class="col-sm-12 c-d-block c-text-truncate">Note : In case of multiple IP addresses for the MikroWizard
server, use<code style="padding: 0!important;">[mikrowizard]</code> instead of the MikroWizard server IP.
</div>
</div> </div>
</c-input-group> </c-input-group>
<br /> <br />
@ -230,27 +307,28 @@
</c-modal-footer> </c-modal-footer>
</c-modal> </c-modal>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" <c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
id="DeleteConfirmModal">
<c-modal-header> <c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedSnippet['name'] }}</h5> <h5 cModalTitle>Confirm delete {{ SelectedSnippet['name'] }}</h5>
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button> <button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
Are you sure that You want to delete following Snippet ? Are you sure that You want to delete following Snippet ?
<br/> <br />
<br/> <br />
<table style="width: 100%;"> <table style="width: 100%;">
<tr> <tr>
<td><b>Snippet name : </b>{{ SelectedSnippet['name'] }} <td><b>Snippet name : </b>{{ SelectedSnippet['name'] }}
</tr> </tr>
<tr> <tr>
</tr> </tr>
<tr> <tr>
<td> <td>
<p ><code style="padding: 0!important;"><b>Warning:</b> ALL <b>Tasks</b> related to this snippet Will be <b>modifed or deleted</b> and stop working!</code></p> <p><code
</td> style="padding: 0!important;"><b>Warning:</b> ALL <b>Tasks</b> related to this snippet Will be <b>modifed or deleted</b> and stop working!</code>
</tr> </p>
</td>
</tr>
</table> </table>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
@ -264,5 +342,3 @@
</c-modal> </c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster> <c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -12,32 +12,19 @@ import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { HttpClient, HttpClientModule, HttpParams } from "@angular/common/http"; import { HttpClient, HttpParams } from "@angular/common/http";
import {
GuiCellView,
GuiSearching,
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiDataType,
GuiPaging,
GuiPagingDisplay,
GuiRowColoring,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { Table } from 'primeng/table';
@Component({ @Component({
templateUrl: "snippets.component.html", templateUrl: "snippets.component.html",
}) })
export class SnippetsComponent implements OnInit, OnDestroy { export class SnippetsComponent implements OnInit, OnDestroy {
public uid: number; public uid!: number;
public uname: string; public uname!: string;
public tz: string; public tz!: string;
public ispro: boolean = false;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
@ -56,7 +43,8 @@ export class SnippetsComponent implements OnInit, OnDestroy {
// console.dir("res",res) // console.dir("res",res)
_self.uid = res.uid; _self.uid = res.uid;
_self.uname = res.name; _self.uname = res.name;
_self.tz = res.tz; _self.tz = res.tz;
_self.ispro = res.ispro;
// console.dir("role",res.role); // console.dir("role",res.role);
const userId = _self.uid; const userId = _self.uid;
@ -73,9 +61,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
} }
} }
@ViewChild("nameSummaryCell") @ViewChild("nameSummaryCell")
nameSummaryCell: TemplateRef<any>; nameSummaryCell!: TemplateRef<any>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = []; public columns: Array<any> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public Selectedrows: any; public Selectedrows: any;
@ -94,6 +82,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
public NewMemberRows: any = []; public NewMemberRows: any = [];
public SelectedNewMemberRows: any; public SelectedNewMemberRows: any;
@ViewChild('dtNewMember') dtNewMember!: Table;
@ViewChild('dtHistory') dtHistory!: Table;
public current_snippet: any = { public current_snippet: any = {
content: "", content: "",
created: "", created: "",
@ -110,44 +101,15 @@ export class SnippetsComponent implements OnInit, OnDestroy {
name: "", name: "",
}; };
public sorting = {
enabled: true,
multiSorting: true,
};
public ip_scanner: any; public ip_scanner: any;
searching: GuiSearching = { applyFilterNewMember($event: any, stringVal: string) {
enabled: true, this.dtNewMember.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
placeholder: "Search Devices", }
};
public paging: GuiPaging = { applyFilterHistory($event: any, stringVal: string) {
enabled: true, this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
page: 1, }
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
ngOnInit(): void { ngOnInit(): void {
this.initGridTable(); this.initGridTable();
@ -182,31 +144,31 @@ export class SnippetsComponent implements OnInit, OnDestroy {
this.ModalAction = "edit"; this.ModalAction = "edit";
} }
} }
show_exec(item:any){ show_exec(item: any) {
var _self=this; var _self = this;
this.SelectedSnippet = item; this.SelectedSnippet = item;
this.ExecutedDataModalVisible = true; this.ExecutedDataModalVisible = true;
this.data_provider this.data_provider
.get_executed_snipet(_self.SelectedSnippet["id"]) .get_executed_snipet(_self.SelectedSnippet["id"])
.then((res) => { .then((res) => {
let index = 1; let index = 1;
_self.ExecutedData= res.map((d: any) => { _self.ExecutedData = res.map((d: any) => {
d.index = index; d.index = index;
d.ended = formatInTimeZone( d.ended = formatInTimeZone(
d.created.split(".")[0] + ".000Z", d.created.split(".")[0] + ".000Z",
_self.tz, _self.tz,
"yyyy-MM-dd HH:mm:ss XXX" "yyyy-MM-dd HH:mm:ss XXX"
); );
d.started = formatInTimeZone( d.started = formatInTimeZone(
d.info.created.split(".")[0] + ".000Z", d.info.created.split(".")[0] + ".000Z",
_self.tz, _self.tz,
"yyyy-MM-dd HH:mm:ss XXX" "yyyy-MM-dd HH:mm:ss XXX"
); );
index += 1; index += 1;
return d; return d;
}); });
_self.DeleteConfirmModalVisible = false; _self.DeleteConfirmModalVisible = false;
}); });
} }
form_changed() { form_changed() {
@ -254,9 +216,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
}); });
} }
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void { onSelectedRowsNewMembers(rows: Array<any>): void {
this.NewMemberRows = rows; this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source); this.SelectedNewMemberRows = rows.map((m: any) => m.source || m);
} }
add_new_members() { add_new_members() {
@ -274,11 +236,11 @@ export class SnippetsComponent implements OnInit, OnDestroy {
submit(action: string) { submit(action: string) {
var _self = this; var _self = this;
this.data_provider this.data_provider
.Exec_snipet(_self.current_snippet, _self.SelectedTaskItems) .Exec_snipet(_self.current_snippet, _self.SelectedTaskItems)
.then((res) => { .then((res) => {
_self.initGridTable(); _self.initGridTable();
}); });
this.ExecSnipetModalVisible = false; this.ExecSnipetModalVisible = false;
} }
@ -287,7 +249,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
this.current_snippet = item; this.current_snippet = item;
this.current_snippet["task_type"] = "snipet_exec"; this.current_snippet["task_type"] = "snipet_exec";
this.current_snippet["selection_type"] = "devices"; this.current_snippet["selection_type"] = "devices";
this.form_changed(); this.form_changed();
this.ExecSnipetModalVisible = true; this.ExecSnipetModalVisible = true;
this.ModalAction = "exec"; this.ModalAction = "exec";
} }
@ -303,9 +265,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
}); });
} }
onSelectedRows(rows: Array<GuiSelectedRow>): void { onSelectedRows(rows: Array<any>): void {
this.rows = rows; this.rows = rows;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id); this.Selectedrows = rows.map((m: any) => (m.source ? m.source.id : m.id));
} }
remove(item: any) { remove(item: any) {
@ -318,7 +280,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
initGridTable(): void { initGridTable(): void {
var _self = this; var _self = this;
_self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { _self.data_provider.get_snippets("", "", "", 0, 1000, false).then((res) => {
_self.source = res.map((x: any) => { _self.source = res.map((x: any) => {
x.created = [ x.created = [
x.created.split("T")[0], x.created.split("T")[0],
@ -330,18 +292,18 @@ export class SnippetsComponent implements OnInit, OnDestroy {
}); });
} }
sanitizeString(desc:string) { sanitizeString(desc: string) {
var itemDesc:string=''; var itemDesc: string = '';
if (desc) { if (desc) {
itemDesc = desc.toString().replace(/"/g, '\"'); itemDesc = desc.toString().replace(/"/g, '\"');
itemDesc = itemDesc.replace(/'/g, '\''); itemDesc = itemDesc.replace(/'/g, '\'');
} else { } else {
itemDesc = ''; itemDesc = '';
} }
return itemDesc; return itemDesc;
} }
exportToCsv(jsonResponse:any) { exportToCsv(jsonResponse: any) {
const data = jsonResponse; const data = jsonResponse;
const columns = this.getColumns(data); const columns = this.getColumns(data);
const csvData = this.convertToCsv(data, columns); const csvData = this.convertToCsv(data, columns);
@ -349,7 +311,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
} }
getColumns(data: any[]): string[] { getColumns(data: any[]): string[] {
const columns : any = []; const columns: any = [];
data.forEach(row => { data.forEach(row => {
Object.keys(row).forEach((col) => { Object.keys(row).forEach((col) => {
if (!columns.includes(col)) { if (!columns.includes(col)) {
@ -361,13 +323,13 @@ export class SnippetsComponent implements OnInit, OnDestroy {
} }
convertToCsv(data: any[], columns: string[]): string { convertToCsv(data: any[], columns: string[]): string {
var _self=this; var _self = this;
let csv = ''; let csv = '';
csv += columns.join(',') + '\n'; csv += columns.join(',') + '\n';
data.forEach(row => { data.forEach(row => {
const values : any = []; const values: any = [];
columns.forEach((col:any) => { columns.forEach((col: any) => {
values.push('"'+_self.sanitizeString(row[col])+'"'); values.push('"' + _self.sanitizeString(row[col]) + '"');
}); });
csv += values.join(',') + '\n'; csv += values.join(',') + '\n';
}); });
@ -376,10 +338,10 @@ export class SnippetsComponent implements OnInit, OnDestroy {
downloadFile(data: string, filename: string, type: string) { downloadFile(data: string, filename: string, type: string) {
const blob = new Blob([data], { type: type }); const blob = new Blob([data], { type: type });
const nav = (window.navigator as any); const nav = (window.navigator as any);
if (nav.msSaveOrOpenBlob) { if (nav.msSaveOrOpenBlob) {
nav.msSaveBlob(blob, filename); nav.msSaveBlob(blob, filename);
} else { } else {
const link = document.createElement('a'); const link = document.createElement('a');
link.setAttribute('href', URL.createObjectURL(blob)); link.setAttribute('href', URL.createObjectURL(blob));
@ -405,5 +367,5 @@ export class SnippetsComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {} ngOnDestroy(): void { }
} }

View file

@ -13,7 +13,9 @@ import {
} from "@coreui/angular"; } from "@coreui/angular";
import { SnippetsRoutingModule } from "./snippets-routing.module"; import { SnippetsRoutingModule } from "./snippets-routing.module";
import { SnippetsComponent } from "./snippets.component"; import { SnippetsComponent } from "./snippets.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
@NgModule({ @NgModule({
imports: [ imports: [
@ -24,7 +26,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule, ModalModule,
ToastModule, ToastModule,
FormsModule, FormsModule,

View file

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SyslogRegexComponent } from './syslog-regex.component';
const routes: Routes = [
{
path: '',
component: SyslogRegexComponent,
data: {
title: 'Syslog Custom Regex'
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SyslogRegexRoutingModule {
}

View file

@ -0,0 +1,571 @@
<div style="position: relative;">
<c-row>
<c-col xs>
<c-card class="mb-4">
<c-card-header>
<c-row>
<c-col xs [lg]="3">
Syslog Custom Regex
</c-col>
<c-col xs [lg]="9">
<h6 style="text-align: right;">
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Regex('','add')"
style="color: #fff;"><i class="fa-solid fa-plus"></i> Add New Regex</button>
<button cButton color="warning" class="mx-1" size="sm" (click)="openManageAlerts()"
style="color: #000;"><i class="fa-solid fa-bell"></i> Manage Alerts</button>
</h6>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<div class="mb-3 d-flex justify-content-end">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search regexes..."
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dt showGridlines [value]="source" [paginator]="true" [rows]="10"
[showCurrentPageReport]="true" [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true"
columnResizeMode="expand" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['name','regex_pattern','match_string']">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
Name
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="regex_pattern" pResizableColumn>
Regex Pattern
<p-sortIcon field="regex_pattern"></p-sortIcon>
<p-columnFilter type="text" field="regex_pattern" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="alert_id" pResizableColumn>Alert / Severity <p-sortIcon
field="alert_id"></p-sortIcon></th>
<th pSortableColumn="alert_enabled" pResizableColumn
style="width: 15rem; text-align: center;">Alert Enabled/Type <p-sortIcon
field="alert_enabled"></p-sortIcon></th>
<th pSortableColumn="match_string" pResizableColumn>
Match String
<p-sortIcon field="match_string"></p-sortIcon>
<p-columnFilter type="text" field="match_string" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="created" pResizableColumn>Created <p-sortIcon
field="created"></p-sortIcon></th>
<th style="width: 10rem; text-align: center;" class="p-resizable-column">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>{{item.name}}</td>
<td><code class="px-1 py-1">{{item.regex_pattern}}</code></td>
<td>
<div class="d-flex flex-row gap-1 align-items-center">
<span class="fw-bold">{{ getAlertName(item.alert_id) }}</span>
<c-badge size="sm"
[color]="getAlertLevel(item.alert_id).toLowerCase() =='critical' ? 'danger' :
getAlertLevel(item.alert_id).toLowerCase() =='warning' ? 'warning' :
getAlertLevel(item.alert_id).toLowerCase() =='info' ? 'info' :
getAlertLevel(item.alert_id).toLowerCase() =='error' ? 'danger' : 'secondary'">{{
getAlertLevel(item.alert_id)}}</c-badge>
</div>
</td>
<td class="text-center">
<i *ngIf="item.alert_enabled" class="fa-solid fa-check mx-1" style="color: green;"></i>
<i *ngIf="!item.alert_enabled" class="fa-solid fa-x mx-1" style="color: red;"></i>
<span *ngIf="item.alert_enabled && item.global_alert"><i
class="fa-solid fa-earth-americas text-primary" title="Global Alert"></i>
Global</span>
<span *ngIf="item.alert_enabled && !item.global_alert"><i
class="fa-solid fa-server text-secondary" title="Device Specific"></i> String
Match</span>
</td>
<td>{{item.match_string}}</td>
<td>{{item.created}}</td>
<td class="text-center">
<button cButton color="primary" size="sm" (click)="Edit_Regex(item,'edit')" class="mx-1"
title="Edit">
<i class="fa-regular fa-pen-to-square"></i>
</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)"
class="mx-1" title="Delete">
<i class="fa-regular fa-trash-can"></i>
</button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="7">No regexes found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body>
</c-card>
</c-col>
</c-row>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<c-modal #EditRegexModal backdrop="static" size="xl" [fullscreen]="true" [(visible)]="EditRegexModalVisible"
id="EditRegexModal">
<c-modal-header class="bg-light">
<h5 *ngIf="ModalAction=='edit'" cModalTitle>
<i class="fa-solid fa-edit me-2"></i>Edit Regex: {{current_regex['name']}}
</h5>
<h5 *ngIf="ModalAction=='add'" cModalTitle>
<i class="fa-solid fa-plus me-2"></i>Add New Regex
</h5>
<button (click)="EditRegexModalVisible=false" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4 bg-light">
<c-card class="mb-4 shadow-sm border-0">
<c-card-header class="bg-white border-bottom-0 pt-3 pb-0">
<h6 class="mb-1"><i class="fa-solid fa-code text-primary me-2"></i>Configuration Details</h6>
</c-card-header>
<c-card-body>
<div class="row g-4">
<!-- LEFT COLUMN: Info & Configuration -->
<div class="col-lg-5">
<!-- SECTION 1: Basic Info & Log Input -->
<div class="mb-4">
<h6 class="border-bottom pb-2 mb-3 text-primary"><i
class="fa-solid fa-info-circle me-2"></i>1. General Information</h6>
<div [cFormFloating]="true" class="mb-4">
<input cFormControl placeholder="e.g. Failed VPN Auth" [(ngModel)]="current_regex.name"
id="regexName" class="border-secondary" />
<label cLabel for="regexName">Regex Name</label>
</div>
<label class="form-label fw-bold small text-muted mb-1">Live Syslog Sample</label>
<p class="small text-muted mb-2">Provide a real log message to build and test your regex
against.</p>
<div class="row g-2 mb-2">
<div class="col-md-5">
<select class="form-select border-secondary shadow-sm bg-light"
[(ngModel)]="selectedSampleIndex" (ngModelChange)="onSampleSelected()">
<option [ngValue]="null" disabled *ngIf="syslogSamples.length > 0">-- Pick from
recent logs --</option>
<option [ngValue]="null" disabled *ngIf="syslogSamples.length === 0">No recent
samples</option>
<option *ngFor="let sample of syslogSamples; let i = index" [ngValue]="i"
[title]="sample">
{{ (sample.length > 50) ? (sample | slice:0:50) + '...' : sample }}
</option>
</select>
</div>
<div class="col-md-7 position-relative">
<textarea class="form-control border-secondary shadow-sm" rows="2"
[(ngModel)]="sampleLog"
(ngModelChange)="evaluateRegexAgainstSample(); updateBuilder()"
placeholder="Paste mock log here..."></textarea>
<div class="backdrop" [innerHTML]="getHighlightedSampleBoxHtml()"
style="padding: 0.375rem 0.75rem; font-size: 0.875rem;"></div>
</div>
</div>
</div>
<!-- SECTION 2: Goal / Alert Configuration -->
<div class="mb-4">
<h6 class="border-bottom pb-2 mb-3 text-primary"><i class="fa-solid fa-bullseye me-2"></i>2.
Event Handling & Alerts</h6>
<p class="small text-muted mb-3">What happens when this regex matches a log? The event will
inherit the title and severity from the chosen Alert.</p>
<div class="card bg-light border-secondary shadow-sm">
<div class="card-body py-3">
<c-form-check class="mb-3">
<input cFormCheckInput type="checkbox" id="alertEnabled"
[(ngModel)]="current_regex.alert_enabled"
(ngModelChange)="evaluateRegexAgainstSample()" />
<label cFormCheckLabel for="alertEnabled" class="fw-bold">Attach to Alert
Definition</label>
</c-form-check>
<div *ngIf="current_regex.alert_enabled" class="border-top pt-3 mt-2">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold mb-1">Alert
Definition</label>
<div class="d-flex">
<select class="form-select border-secondary"
[(ngModel)]="current_regex.alert_id"
(ngModelChange)="evaluateRegexAgainstSample()">
<option [ngValue]="null" disabled>Select Alert...</option>
<option *ngFor="let a of Alerts" [value]="a.id">{{a.name}}
({{a.level}})</option>
</select>
<button cButton color="warning" variant="outline" class="ms-2"
(click)="openManageAlerts()" title="Manage Alerts"><i
class="fa-solid fa-cog"></i></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold mb-1">Storage
Condition</label>
<select class="form-select border-secondary"
[(ngModel)]="current_regex.alert_mode">
<option value="global">Always Store (Global)</option>
<option value="conditional">Only if String Matches (Conditional)
</option>
</select>
<div *ngIf="current_regex.alert_mode === 'conditional'" class="mt-2">
<input cFormControl placeholder="e.g. error"
[(ngModel)]="current_regex.match_string"
class="border-secondary" />
</div>
</div>
</div>
</div>
<div class="mt-1 border-top pt-3" *ngIf="!current_regex.alert_enabled">
<small class="text-danger d-block"><i
class="fa-solid fa-triangle-exclamation me-1"></i>Regex matching will
occur, but no alert,soring in db or action will be
applied.</small>
</div>
<!-- Fallbacks -->
<div class="row g-3 mt-2 border-top pt-3" *ngIf="current_regex.alert_enabled">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold mb-1">Default Event
Type</label>
<select class="form-select form-select-sm border-secondary"
[(ngModel)]="current_regex.eventtype"
(ngModelChange)="evaluateRegexAgainstSample()">
<option value="">-- Auto --</option>
<option value="state">state</option>
<option value="connection">connection</option>
<option value="config">config</option>
<option value="firmware">firmware</option>
<option value="health">health</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold mb-1">Default
Status</label>
<select class="form-select form-select-sm border-secondary"
[(ngModel)]="current_regex.status"
(ngModelChange)="evaluateRegexAgainstSample()">
<option [ngValue]="0">0 - Open (not fixed)</option>
<option [ngValue]="1">1 - Fixed (resolved)</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div> <!-- END LEFT COLUMN -->
<!-- RIGHT COLUMN: Builder & Preview -->
<div class="col-lg-7 border-start ps-4">
<!-- SECTION 3: Regex Builder -->
<div class="mb-4">
<h6 class="border-bottom pb-2 mb-3 text-primary"><i class="fa-solid fa-code me-2"></i>3.
Pattern Extraction</h6>
<ul class="nav nav-tabs nav-justified mb-3">
<li class="nav-item">
<a class="nav-link cursor-pointer text-dark fw-bold"
[class.active]="inputMode === 'builder'" (click)="setMode('builder')">
<i class="fa-solid fa-wand-magic-sparkles me-2 text-primary"></i>Visual Builder
</a>
</li>
<li class="nav-item">
<a class="nav-link cursor-pointer text-dark fw-bold"
[class.active]="inputMode === 'raw'" (click)="setMode('raw')">
<i class="fa-solid fa-terminal me-2 text-danger"></i>Raw Regex
</a>
</li>
<li class="nav-item ms-auto" *ngIf="sampleLog">
<button cButton color="info" variant="outline" size="sm"
class="fw-bold mt-1 animate-fade-in" (click)="openAIHelp()"
title="Ask AI for help with regex">
<i class="fa-solid fa-robot me-1"></i> AI Help
</button>
</li>
</ul>
<!-- RAW MODE -->
<div *ngIf="inputMode === 'raw'" class="p-3 border border-top-0 rounded-bottom bg-light">
<div [cFormFloating]="true" class="mb-1">
<input cFormControl placeholder="e.g. vpn_error: (.*)"
[(ngModel)]="current_regex.regex_pattern" (ngModelChange)="onRawRegexChange()"
id="regexPattern" class="border-secondary font-monospace"
[class.is-invalid]="rawRegexError" />
<label cLabel for="regexPattern">Python Regex Pattern</label>
</div>
<small *ngIf="rawRegexError" class="text-danger d-block mb-2"><i
class="fa-solid fa-triangle-exclamation me-1"></i>{{rawRegexError}}</small>
<small class="text-muted"><i
class="fa-solid fa-circle-info me-1 mt-1 text-primary"></i>Only the
<code class="px-1 py-1">(?P&lt;comment&gt;...)</code> group will modify the stored
event
message.</small>
</div>
<!-- BUILDER MODE -->
<div *ngIf="inputMode === 'builder'"
class="p-3 border border-top-0 rounded-bottom bg-light">
<div class="d-flex justify-content-between mb-3">
<span class="small fw-bold text-muted mt-1">Add segments to match the log
structurally:</span>
<div class="btn-group btn-group-sm shadow-sm">
<button class="btn btn-outline-primary" (click)="addSegment('static')"><i
class="fa-solid fa-plus me-1"></i>Static Text</button>
<button class="btn btn-outline-success" (click)="addSegment('dynamic')"><i
class="fa-solid fa-plus me-1"></i>Dynamic Data</button>
</div>
</div>
<div class="segments-container border border-secondary p-2 rounded bg-white">
<div *ngFor="let seg of segments; let i = index"
class="segment-row d-flex align-items-start gap-2 mb-2 p-2 rounded bg-light border position-relative">
<!-- Ordering handles -->
<div class="d-flex flex-column justify-content-center align-items-center me-1"
style="width: 20px;">
<button class="btn btn-sm btn-link p-0 text-muted"
(click)="moveSegmentUp(i)" [disabled]="i === 0"><i
class="fa-solid fa-caret-up"></i></button>
<span class="badge bg-secondary rounded-circle"
style="font-size: 0.6rem;">{{i+1}}</span>
<button class="btn btn-sm btn-link p-0 text-muted"
(click)="moveSegmentDown(i)" [disabled]="i === segments.length - 1"><i
class="fa-solid fa-caret-down"></i></button>
</div>
<div class="flex-grow-1">
<!-- Static Segment -->
<div *ngIf="seg.type === 'static'" class="row g-2 align-items-center">
<div class="col-md-3">
<span class="badge bg-secondary text-white"><i
class="fa-solid fa-font me-1"></i>Static Match</span>
</div>
<div class="col-md-9">
<input type="text"
class="form-control form-control-sm border-secondary fw-bold"
[(ngModel)]="seg.value" (ngModelChange)="updateBuilder()"
placeholder="Exact text to match...">
</div>
</div>
<!-- Dynamic Segment -->
<div *ngIf="seg.type === 'dynamic'" class="row g-2 align-items-center">
<div class="col-md-3">
<span class="badge bg-success text-white"><i
class="fa-solid fa-wand-magic-sparkles me-1"></i>Extract
Data</span>
<select class="form-select form-select-sm fw-semibold mt-1"
[(ngModel)]="seg.captureType" (ngModelChange)="updateBuilder()">
<option value="everything">Everything (.*)</option>
<option value="word">Word (\S+)</option>
<option value="ip">IP Addr</option>
<option value="number">Number</option>
<option value="custom">Custom...</option>
</select>
</div>
<div class="col-md-9">
<span class="badge bg-primary text-white"><i
class="fa-solid fa-map-marker-alt me-1"></i>Map to
Field</span>
<input *ngIf="seg.captureType === 'custom'" type="text"
class="form-control form-control-sm mb-1 border-secondary font-monospace"
[(ngModel)]="seg.value" (ngModelChange)="updateBuilder()"
placeholder="Custom Regex: [a-zA-Z]+">
<select
class="form-select form-select-sm fw-bold border-success text-success bg-white"
[(ngModel)]="seg.targetField" (ngModelChange)="updateBuilder()">
<option [ngValue]="undefined">Discard (Do not save)</option>
<option value="comment">Set to Detail
</option>
</select>
</div>
</div>
</div>
<button class="btn btn-sm btn-outline-danger mt-1" (click)="removeSegment(i)"
*ngIf="segments.length > 1" title="Remove"><i
class="fa-solid fa-trash"></i></button>
</div>
<div *ngIf="segments.length === 0" class="text-center p-3 text-muted small">No
extraction blocks yet.</div>
</div>
</div>
</div>
<div
class="step-card mt-3 p-3 border border-secondary rounded shadow-sm bg-dark text-white border-top border-warning border-3">
<h6 class="text-warning mb-3 fw-bold"><i class="fa-solid fa-flask text-warning me-2"></i>4.
Live Validation Output</h6>
<!-- Snippet showing the raw string being executed -->
<div class="mb-3" *ngIf="inputMode === 'builder'">
<small class="text-muted d-block mb-1 text-uppercase fw-bold"
style="font-size: 0.65rem;">Engine Pattern</small>
<code
class="d-block px-2 py-2 text-warning bg-black mx-0 rounded border border-secondary font-monospace lh-base"
style="font-size: 0.85rem; word-break: break-all;">{{generatedRegex || '(empty)'}}</code>
</div>
<div class="d-flex align-items-center mb-2 mt-2">
<div class="test-indicator me-2 shadow-sm rounded-circle"
style="width: 12px; height: 12px;" [class.bg-success]="isTestValid"
[class.bg-danger]="!isTestValid"
*ngIf="sampleLog && (inputMode === 'raw' ? current_regex.regex_pattern : generatedRegex)">
</div>
<span [class.text-success]="isTestValid" [class.text-danger]="!isTestValid"
class="fw-bold fs-6">{{testFeedback || 'Awaiting valid configuration...'}}</span>
</div>
<div *ngIf="isTestValid && simulatedDbRow" class="mt-3 text-dark">
<div class="table-responsive rounded border border-secondary bg-white shadow-sm">
<table class="table table-hover table-bordered mb-0 align-middle"
style="font-size: 0.85rem;">
<thead class="bg-light text-center">
<tr>
<th class="py-2" style="width: 20%">Category</th>
<th class="py-2" style="width: 22%">Event</th>
<th class="py-2" style="width: 12%">Level</th>
<th class="py-2" style="width: 44%">Detail</th>
<th class="py-2" style="width: 12%">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span class="badge w-75 py-2 fw-semibold"
[ngClass]="simulatedDbRow.eventtype ? 'bg-primary' : 'bg-secondary'">{{simulatedDbRow.eventtype
|| 'null'}}</span></td>
<td class="text-wrap fw-bold ps-2">{{simulatedDbRow.detail || 'null'}}
</td>
<td class="text-center">
<span class="badge w-75 py-2 fw-bold" [ngClass]="{
'bg-info': simulatedDbRow.level?.toLowerCase() === 'info',
'bg-warning text-dark': simulatedDbRow.level?.toLowerCase() === 'warning' || simulatedDbRow.level?.toLowerCase() === 'config',
'bg-danger': simulatedDbRow.level?.toLowerCase() === 'critical' || simulatedDbRow.level?.toLowerCase() === 'error',
'bg-success': simulatedDbRow.level?.toLowerCase() === 'health' || simulatedDbRow.level?.toLowerCase() === 'state'
}">{{simulatedDbRow.level || 'null'}}</span>
</td>
<td class="text-wrap text-muted font-monospace bg-light ps-2 fs-6">
{{simulatedDbRow.comment}}</td>
<td class="text-center">
<span class="badge w-75 py-2 fw-bold" [ngClass]="{
'bg-success': simulatedDbRow.status == '1',
'bg-danger': simulatedDbRow.status == '0'
}">{{simulatedDbRow.status == '1' ? 'Fixed' : 'Not
Fixed'}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div> <!-- END RIGHT COLUMN -->
</div> <!-- END ROW G-4 -->
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer class="bg-white border-top-0 pt-0 pb-3 pe-4">
<button cButton color="secondary" variant="ghost" (click)="EditRegexModalVisible=false">Cancel</button>
<button cButton color="primary" class="px-4" (click)="save_regex()"
[disabled]="(inputMode === 'raw' && rawRegexError !== '') || (inputMode === 'builder' && generatedRegex === '')">
<i class="fa-solid fa-save me-2"></i>Save Regex
</button>
</c-modal-footer>
</c-modal>
<!-- Manage Alerts Modal -->
<c-modal #ManageAlertsModal backdrop="static" size="lg" [(visible)]="ManageAlertsModalVisible" id="ManageAlertsModal">
<c-modal-header class="bg-light">
<h5 cModalTitle><i class="fa-solid fa-bell me-2"></i>Manage Alert Definitions</h5>
<button (click)="ManageAlertsModalVisible=false" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4 bg-light">
<c-row>
<!-- Form Column -->
<c-col xs="12" md="4" class="mb-4">
<c-card class="shadow-sm border-0">
<c-card-header class="bg-white">
<h6 class="mb-0">{{editingAlert.id === 0 ? 'Create Alert' : 'Edit Alert'}}</h6>
</c-card-header>
<c-card-body>
<div class="mb-3">
<label class="form-label small text-muted">Alert Name</label>
<input class="form-control" [(ngModel)]="editingAlert.name" placeholder="e.g. CPU High">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Severity Level</label>
<select class="form-select" [(ngModel)]="editingAlert.level">
<option value="Warning">Warning</option>
<option value="Critical">Critical</option>
<option value="Error">Error</option>
</select>
</div>
<button cButton color="primary" class="w-100" (click)="saveAlert()">
<i class="fa-solid fa-save me-1"></i>Save Alert
</button>
<button *ngIf="editingAlert.id !== 0" cButton color="secondary" variant="ghost"
class="w-100 mt-2" (click)="editingAlert = { id: 0, name: '', level: 'info' }">Cancel
Edit</button>
</c-card-body>
</c-card>
</c-col>
<!-- List Column -->
<c-col xs="12" md="8">
<c-card class="shadow-sm border-0">
<c-card-header class="bg-white">
<h6 class="mb-0">Existing Alerts</h6>
</c-card-header>
<c-card-body class="p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th>Name</th>
<th>Level</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let alert of Alerts">
<td class="fw-semibold">{{alert.name}}</td>
<td>
<c-badge
[color]="alert.level === 'critical' ? 'danger' : (alert.level === 'warning' ? 'warning' : 'info')">
{{alert.level | uppercase}}
</c-badge>
</td>
<td class="text-end">
<button cButton color="info" size="sm" variant="ghost" class="me-1"
(click)="editAlert(alert)"><i class="fa-solid fa-edit"></i></button>
<button cButton color="danger" size="sm" variant="ghost"
(click)="deleteAlert(alert.id)"><i
class="fa-solid fa-trash"></i></button>
</td>
</tr>
<tr *ngIf="Alerts.length === 0">
<td colspan="3" class="text-center text-muted py-4">No alerts defined.</td>
</tr>
</tbody>
</table>
</div>
</c-card-body>
</c-card>
</c-col>
</c-row>
</c-modal-body>
</c-modal>

View file

@ -0,0 +1,125 @@
.alert-config-box {
transition: all 0.3s ease;
border-color: #e2e8f0 !important;
}
code {
background-color: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 4px;
color: #d63384;
font-size: 0.875em;
}
/* Regex Builder Styles */
.cursor-pointer {
cursor: pointer;
}
.test-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.step-title {
font-size: 0.95rem;
font-weight: 600;
}
/* Highlighter Textarea Overlay Trick */
.position-relative {
position: relative;
}
textarea.form-control {
position: relative;
z-index: 2;
background: transparent !important;
color: transparent !important;
/* Hide real text to only show highlights behind it, but keep caret visible */
caret-color: #000;
}
.backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
padding: 0.375rem 0.75rem;
/* Match textarea padding */
font-size: 0.8rem;
/* Match small text if needed */
line-height: 1.5;
color: #495057;
background-color: #fff;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid transparent;
pointer-events: none;
border-radius: 0.375rem;
overflow-y: auto;
}
.segment-row {
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid #edf2f7 !important;
&:hover {
background-color: #ffffff !important;
border-color: #90cdf4 !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transform: translateY(-2px);
}
.btn-link {
opacity: 0.3;
transition: opacity 0.2s;
}
&:hover .btn-link {
opacity: 1;
}
}
.test-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.step-card {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
}
}
.table-dark {
--cui-table-bg: transparent;
--cui-table-color: #fff;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.4s ease-out forwards;
}

View file

@ -0,0 +1,590 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Table } from 'primeng/table';
import { dataProvider } from "../../providers/mikrowizard/data";
import { NgxSuperSelectOptions } from "ngx-super-select";
import { ToastComponent } from '@coreui/angular';
interface RegexSegment {
type: 'static' | 'dynamic';
value: string; // Used for static text
captureType?: string; // Used for dynamic: everything, word, ip, mac
targetField?: string; // Mapping: src, detail, level, status, comment
}
@Component({
selector: 'app-syslog-regex',
templateUrl: './syslog-regex.component.html',
styleUrls: ['./syslog-regex.component.scss']
})
export class SyslogRegexComponent implements OnInit {
@ViewChild('dt') dt: Table | undefined;
public syslogRegexes: any[] = [];
// Grid config
source: Array<any> = [];
searching = {
enabled: true,
placeholder: 'Search regexes...'
};
paging = {
enabled: true,
pageSize: 10,
pageSizes: [10, 25, 50]
};
columnMenu = { enabled: false };
sorting = { enabled: true };
infoPanel = { enabled: true };
rowSelection: any = {
enabled: true,
type: 'checkbox',
mode: 'multiple',
};
public EditRegexModalVisible: boolean = false;
public current_regex: any = {
id: 0,
name: '',
regex_pattern: '',
alert_enabled: false,
alert_id: null,
global_alert: true,
match_string: '',
alert_mode: 'global',
default_eventtype: '',
default_status: 0
};
// Builder vs Raw Mode
public inputMode: 'raw' | 'builder' = 'raw';
public rawRegexError: string = '';
public sampleLog: string = '';
// Segment-based Builder State
public segments: RegexSegment[] = [];
public generatedRegex: string = '';
public testFeedback: string = '';
public testResults: { field: string, value: string }[] = [];
public simulatedDbRow: { eventtype: string, src: string, detail: string, level: string, status: string, comment: string } | null = null;
public isTestValid: boolean = false;
public extractedValue: string = '';
public syslogSamples: string[] = [];
public selectedSampleIndex: number | null = null;
public ManageAlertsModalVisible: boolean = false;
public editingAlert: any = { id: 0, name: '', level: 'Info' };
public Alerts: any = [];
public ModalAction: string = "add";
// super select options
alertOptions: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Select Alert",
searchEnabled: true,
};
constructor(public MikroWizardRPC: dataProvider) { }
ngOnInit(): void {
this.loadRegexes();
this.loadAlerts();
}
applyFilterGlobal($event: any, stringVal: string) {
this.dt!.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
loadRegexes() {
this.MikroWizardRPC.get_syslog_regexes().then(
(res: any) => {
if (res) {
this.source = res;
}
},
(error: any) => {
// Handle error visually if needed
}
);
}
loadAlerts(): void {
this.MikroWizardRPC.get_alerts().then((res: any) => {
this.Alerts = res.map((x: any) => ({ id: x.id, name: x.name, level: x.level }));
});
}
resetBuilder() {
this.segments = [];
this.generatedRegex = '';
this.testFeedback = '';
this.testResults = [];
this.simulatedDbRow = null;
this.extractedValue = '';
this.selectedSampleIndex = null;
this.loadSyslogSamples();
}
Edit_Regex(item: any, mode: string) {
// Reset builder state
this.inputMode = 'raw';
this.rawRegexError = '';
this.resetBuilder();
if (mode === 'add') {
this.current_regex = {
id: 0,
name: '',
regex_pattern: '',
alert_enabled: false,
alert_id: null,
global_alert: true,
match_string: '',
alert_mode: 'global',
eventtype: '',
status: 0
};
this.ModalAction = 'add';
} else {
// deep copy
this.current_regex = JSON.parse(JSON.stringify(item));
this.current_regex.alert_mode = this.current_regex.global_alert ? 'global' : 'conditional';
// Try to deconstruct the regex into builder segments
if (this.current_regex.regex_pattern) {
const wasDeconstructed = this.deconstructRegex(this.current_regex.regex_pattern);
if (wasDeconstructed) {
this.inputMode = 'builder';
}
}
this.ModalAction = 'edit';
}
// Initial validation
this.validateRawRegex();
this.EditRegexModalVisible = true;
}
save_regex() {
if (this.inputMode === 'builder') {
this.current_regex.regex_pattern = this.generatedRegex;
}
// Final validation check
this.validateRawRegex();
if (this.rawRegexError && this.inputMode === 'raw') {
return; // Prevent save if invalid
}
if (!this.isTestValid && this.inputMode === 'builder') {
if (!this.generatedRegex) return;
}
let payload = { ...this.current_regex };
// Ensure alert_id is null if not alert_enabled
if (!payload.alert_enabled) {
payload.alert_id = null;
payload.global_alert = false;
payload.match_string = '';
} else {
if (payload.alert_mode === 'global') {
payload.global_alert = true;
payload.match_string = '';
} else {
payload.global_alert = false;
if (!payload.match_string) {
alert("Please provide a Match String for conditional alert storage.");
return;
}
}
}
// Remove internal properties and level (now inherited from alert)
delete payload.alert_mode;
delete payload.level;
// Ensure alert_id is mandatory for save (as per v3.2)
if (payload.alert_enabled && !payload.alert_id) {
alert("Please select an Alert Definition.");
return;
}
this.MikroWizardRPC.save_syslog_regex(payload).then(() => {
this.EditRegexModalVisible = false;
this.loadRegexes();
});
}
confirm_delete(item: any, confirm: boolean) {
if (!confirm) {
if (window.confirm("Are you sure you want to delete custom regex: " + item.name + "?")) {
this.MikroWizardRPC.delete_syslog_regex(item.id).then(() => {
this.loadRegexes();
});
}
}
}
// --- Alert Management ---
openManageAlerts() {
this.ManageAlertsModalVisible = true;
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
}
editAlert(item: any) {
this.editingAlert = { ...item };
}
saveAlert() {
this.MikroWizardRPC.save_alert(this.editingAlert).then(() => {
this.loadAlerts();
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
this.loadRegexes();
});
}
deleteAlert(id: number) {
if (confirm("Are you sure you want to delete this alert?")) {
this.MikroWizardRPC.delete_alert(id).then(() => {
this.loadAlerts();
});
}
}
// --- Syslog Samples Management ---
loadSyslogSamples() {
this.MikroWizardRPC.get_syslogregex_samples().then(
(res: any) => {
if (res && Array.isArray(res)) {
this.syslogSamples = res;
}
},
(error: any) => {
console.error("Failed to load syslog samples", error);
}
);
}
onSampleSelected() {
if (this.selectedSampleIndex !== null && this.syslogSamples[this.selectedSampleIndex]) {
this.sampleLog = this.syslogSamples[this.selectedSampleIndex];
this.updateBuilder();
}
}
getAlertName(id: any): string {
const alert = this.Alerts.find((a: any) => Number(a.id) === Number(id));
return alert ? alert.name : 'No Alert';
}
getAlertLevel(id: any): string {
const alert = this.Alerts.find((a: any) => Number(a.id) === Number(id));
return alert ? alert.level : 'N/A';
}
openAIHelp() {
if (!this.sampleLog) return;
const currentRegexText = this.current_regex.regex_pattern ? `\nOptional context - my current attempt is:\n"${this.current_regex.regex_pattern}"` : "";
const prompt = `I need help creating a Python regex pattern (re module syntax) for a specific syslog message.
### My Syslog Sample:
"${this.sampleLog}"
### Requirement:
Create a regex that extracts the most important information from this log into a named capture group called "comment" using the syntax: (?P<comment>...)
${currentRegexText}
### Example Format:
If the log was "System error: Disk full", the regex should be "System error: (?P<comment>.*)".
Please provide the completed Python regex for my sample log.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://chatgpt.com/?q=${encodedPrompt}`, '_blank');
}
// --- Segment Management ---
addSegment(type: 'static' | 'dynamic') {
if (type === 'static') {
this.segments.push({ type: 'static', value: '' });
} else {
this.segments.push({ type: 'dynamic', value: '', captureType: 'everything' });
}
this.updateBuilder();
}
removeSegment(index: number) {
this.segments.splice(index, 1);
this.updateBuilder();
}
moveSegmentUp(index: number) {
if (index > 0) {
const temp = this.segments[index - 1];
this.segments[index - 1] = this.segments[index];
this.segments[index] = temp;
this.updateBuilder();
}
}
moveSegmentDown(index: number) {
if (index < this.segments.length - 1) {
const temp = this.segments[index + 1];
this.segments[index + 1] = this.segments[index];
this.segments[index] = temp;
this.updateBuilder();
}
}
// --- Regex Builder Deconstruction ---
deconstructRegex(pattern: string): boolean {
// This is a specialized parser to turn regex strings back into segments
if (!pattern) return false;
// Common patterns we use
const capturePatterns: { [key: string]: string } = {
'word': '\\S+',
'ip': '\\d{1,3}(?:\\.\\d{1,3}){3}',
'mac': '[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}',
'number': '\\d+',
'everything': '.*'
};
try {
// Split by the delimiter we use in updateBuilder: "\\s*"
const parts = pattern.split('\\s*');
const newSegments: any[] = [];
for (const part of parts) {
if (!part) continue;
// Match a named capture group: (?P<name>pattern)
const namedMatch = part.match(/^\(\?P<([^>]+)>(.+)\)$/);
if (namedMatch) {
const field = namedMatch[1];
const innerPattern = namedMatch[2];
// Try to identify the capture type
let cType = 'custom';
let cValue = innerPattern;
for (const [type, p] of Object.entries(capturePatterns)) {
if (innerPattern === p) {
cType = type;
cValue = '';
break;
}
}
newSegments.push({
type: 'dynamic',
captureType: cType,
targetField: field,
value: cType === 'custom' ? cValue : ''
});
continue;
}
// Match a positional capture group: (pattern)
const groupMatch = part.match(/^\((.+)\)$/);
if (groupMatch) {
const innerPattern = groupMatch[1];
let cType = 'custom';
let cValue = innerPattern;
for (const [type, p] of Object.entries(capturePatterns)) {
if (innerPattern === p) {
cType = type;
cValue = '';
break;
}
}
newSegments.push({
type: 'dynamic',
captureType: cType,
targetField: undefined,
value: cType === 'custom' ? cValue : ''
});
continue;
}
// Otherwise, it's static (or a complex regex part we treat as static)
// We unescape things that we know we escape
let val = part.replace(/\\([.*+?^${}()|[\]\\])/g, '$1');
newSegments.push({ type: 'static', value: val });
}
if (newSegments.length > 0) {
this.segments = newSegments;
this.updateBuilder(); // Refresh generatedRegex and simulations
return true;
}
} catch (e) {
console.error("Failed to deconstruct regex:", e);
}
return false;
}
// --- Regex Builder & Validation Methods ---
validateRawRegex() {
this.rawRegexError = '';
if (!this.current_regex.regex_pattern) {
this.rawRegexError = 'Regex pattern is required.';
return;
}
try {
new RegExp(this.current_regex.regex_pattern);
if (!/\(.*\)/.test(this.current_regex.regex_pattern)) {
this.rawRegexError = 'Must contain at least one capturing group ().';
}
} catch (e) {
this.rawRegexError = 'Invalid regular expression syntax.';
}
}
onRawRegexChange() {
this.validateRawRegex();
if (this.inputMode === 'raw') {
this.evaluateRegexAgainstSample();
}
}
setMode(mode: 'raw' | 'builder') {
this.inputMode = mode;
if (mode === 'builder') {
this.updateBuilder();
} else {
if (this.generatedRegex && this.isTestValid) {
this.current_regex.regex_pattern = this.generatedRegex;
}
this.validateRawRegex();
this.evaluateRegexAgainstSample();
}
}
escapeRegex(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
updateBuilder() {
let pattern = '';
this.segments.forEach((seg, index) => {
if (seg.type === 'static') {
if (seg.value) {
let escaped = this.escapeRegex(seg.value);
pattern += escaped.trim();
}
} else {
let capturePattern = '(.*)';
if (seg.captureType === 'word') capturePattern = '(\\S+)';
if (seg.captureType === 'ip') capturePattern = '(\\d{1,3}(?:\\.\\d{1,3}){3})';
if (seg.captureType === 'mac') capturePattern = '([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})';
if (seg.captureType === 'number') capturePattern = '(\\d+)';
if (seg.captureType === 'custom' && seg.value) capturePattern = `(${seg.value})`;
if (seg.targetField) {
pattern += `(?P<${seg.targetField}>${capturePattern.slice(1, -1)})`;
} else {
pattern += capturePattern;
}
}
if (index < this.segments.length - 1) {
pattern += '\\s*';
}
});
this.generatedRegex = pattern;
this.evaluateRegexAgainstSample();
}
evaluateRegexAgainstSample() {
this.testFeedback = '';
this.isTestValid = false;
this.testResults = [];
this.simulatedDbRow = null;
let regexToTest = this.inputMode === 'raw' ? this.current_regex.regex_pattern : this.generatedRegex;
if (!this.sampleLog || !regexToTest) {
return;
}
try {
let jsPattern = regexToTest.replace(/\(\?P</g, '(?<');
let re = new RegExp(jsPattern);
let match = re.exec(this.sampleLog);
if (match) {
this.isTestValid = true;
this.testFeedback = 'Matches Found!';
this.testResults.push({ field: 'Full Match', value: match[0] });
// Find the alert based on the select element matching
const alertIdToMatch = this.current_regex.alert_id ? Number(this.current_regex.alert_id) : null;
const selectedAlert = this.Alerts.find((a: any) => Number(a.id) === alertIdToMatch);
let detailValue = '(Inherited from Alert)';
let levelValue = '(Inherited from Alert)';
if (this.current_regex.alert_enabled && selectedAlert) {
detailValue = selectedAlert.name;
levelValue = selectedAlert.level;
} else if (!this.current_regex.alert_enabled) {
detailValue = '(No Alert Linked)';
levelValue = '(No Alert Linked)';
}
let dbRow = {
eventtype: this.current_regex.eventtype || '',
src: 'custom regex',
detail: detailValue,
level: levelValue,
status: this.current_regex.status !== undefined ? this.current_regex.status.toString() : '0',
comment: this.sampleLog
};
if (match.groups) {
Object.keys(match.groups).forEach(key => {
this.testResults.push({ field: `[Mapped: ${key}]`, value: match.groups![key] });
if (key === 'comment') {
(dbRow as any)[key] = match.groups![key];
}
});
} else if (match.length > 1) {
for (let i = 1; i < match.length; i++) {
this.testResults.push({ field: `Capture ${i}`, value: match[i] });
}
}
this.simulatedDbRow = dbRow;
} else {
this.testFeedback = "Pattern doesn't match the sample log.";
}
} catch (e: any) {
this.testFeedback = 'Regex Error: ' + e.message;
}
}
getHighlightedSampleBoxHtml(): string {
if (!this.sampleLog || !this.generatedRegex || !this.isTestValid) return this.sampleLog || '';
try {
let jsPattern = this.generatedRegex.replace(/\(\?P</g, '(?<');
let re = new RegExp(`(${jsPattern})`, 'i');
return this.sampleLog.replace(re, '<mark class="bg-warning text-dark">$1</mark>');
} catch (e) {
return this.sampleLog;
}
}
}

View file

@ -0,0 +1,60 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import {
ButtonGroupModule,
ButtonModule,
CardModule,
FormModule,
GridModule,
ToastModule,
ModalModule,
BadgeModule,
AccordionModule,
NavModule,
TabsModule,
AlertModule,
CollapseModule
} from "@coreui/angular";
import { HighlightJsModule } from 'ngx-highlight-js';
import { SyslogRegexRoutingModule } from "./syslog-regex-routing.module";
import { SyslogRegexComponent } from "./syslog-regex.component";
import { NgxSuperSelectModule } from "ngx-super-select";
import { TableModule } from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { MultiSelectModule } from 'primeng/multiselect';
import { TooltipModule } from 'primeng/tooltip';
import { DropdownModule } from 'primeng/dropdown';
import { SharedModule } from "../../shared/shared.module";
@NgModule({
imports: [
SyslogRegexRoutingModule,
CardModule,
CommonModule,
GridModule,
FormModule,
ButtonModule,
ButtonGroupModule,
TableModule,
InputTextModule,
MultiSelectModule,
TooltipModule,
DropdownModule,
ModalModule,
ToastModule,
FormsModule,
BadgeModule,
NgxSuperSelectModule,
AccordionModule,
NavModule,
TabsModule,
AlertModule,
HighlightJsModule,
CollapseModule,
SharedModule
],
declarations: [SyslogRegexComponent],
})
export class SyslogRegexModule { }

View file

@ -8,7 +8,7 @@ const routes: Routes = [
path: '', path: '',
component: SyslogComponent, component: SyslogComponent,
data: { data: {
title: $localize`Mikrowizard System Logs` title: $localize`System Logs`
} }
} }
]; ];

View file

@ -4,9 +4,11 @@
<c-card-header> <c-card-header>
<c-row> <c-row>
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;"> <c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
<h5>Devices</h5> <h5>System Logs</h5>
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']"> <c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time. *ngIf="!filters['start_time'] && !filters['end_time']">
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
default. Use filters to modify the date and time.
</c-alert> </c-alert>
</c-col> </c-col>
<c-col xs [lg]="1"> <c-col xs [lg]="1">
@ -62,44 +64,144 @@
</div> </div>
</c-row> </c-row>
<gui-grid wid [rowDetail]="rowDetail" [horizontalGrid]="true" [rowHeight]="52" [source]="source" <div class="mb-3 d-flex justify-content-end">
[columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left">
[autoResizeWidth]="true"> <i class="pi pi-search"></i>
<gui-grid-column header="#No" type="NUMBER" field="index" width="1" align="CENTER"> <input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
<ng-template let-value="item.index" let-item="item" let-index="index"> class="form-control-sm" />
{{ value }} </span>
</ng-template> </div>
</gui-grid-column>
<gui-grid-column header="username" field="username"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<ng-template let-value="item.username" let-item="item" let-index="index"> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
<div class="gui-dev-info"> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
<span class="gui-dev-info-name">{{ value }}</span> [globalFilterFields]="['username','section','action','ip']" selectionMode="single"
<span class="gui-dev-info-ip">{{ item.first_name }} {{ item.last_name }}</span> (onRowSelect)="showLogDetails($event.data)">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
<div class="justify-between">
<span>#No</span>
<p-sortIcon field="index"></p-sortIcon>
</div>
</th>
<th pSortableColumn="username" pResizableColumn>
<div class="justify-between">
<span>Username</span>
<span>
<p-sortIcon field="username"></p-sortIcon>
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="section" pResizableColumn>
<div class="justify-between">
<span>Section</span>
<span>
<p-sortIcon field="section"></p-sortIcon>
<p-columnFilter type="text" field="section" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="action" pResizableColumn>
<div class="justify-between">
<span>Action</span>
<span>
<p-sortIcon field="action"></p-sortIcon>
<p-columnFilter type="text" field="action" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="ip" pResizableColumn>
<div class="justify-between">
<span>IP</span>
<span>
<p-sortIcon field="ip"></p-sortIcon>
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="created" pResizableColumn>
<div class="justify-between">
<span>Time</span>
<span>
<p-sortIcon field="created"></p-sortIcon>
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
</span>
</div>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr [pSelectableRow]="item" style="cursor: pointer;">
<td>{{item.index}}</td>
<td>
<div class="gui-dev-info">
<span class="gui-dev-info-name">{{ item.username }}</span>
<span class="gui-dev-info-ip" style="font-size: 0.75rem; color: #666;">{{ item.first_name }} {{
item.last_name }}</span>
</div>
</td>
<td>{{item.section}}</td>
<td>{{item.action}}</td>
<td>{{item.ip}}</td>
<td>{{item.created}}</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="6">No logs found.</td>
</tr>
</ng-template>
</p-table>
<!-- Details Drawer -->
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '450px'}" header="Log Details">
<div *ngIf="selectedLog" class="p-3">
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-info-circle me-2 text-primary"></i>System Log</h5>
<div class="row mb-2">
<div class="col-4 fw-bold">Section:</div>
<div class="col-8">{{selectedLog.section}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Action:</div>
<div class="col-8">{{selectedLog.action}}</div>
</div>
<div class="row mb-4">
<div class="col-4 fw-bold">Time:</div>
<div class="col-8 text-muted small">{{selectedLog.created}}</div>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-user me-2 text-primary"></i>User Detail</h5>
<div class="row mb-2">
<div class="col-4 fw-bold">User:</div>
<div class="col-8">{{selectedLog.username}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Name:</div>
<div class="col-8">{{selectedLog.first_name}} {{selectedLog.last_name}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">IP:</div>
<div class="col-8">{{selectedLog.ip}}</div>
</div>
<div class="row mb-2">
<div class="col-4 fw-bold">Agent:</div>
<div class="col-8 small text-wrap text-break" style="max-height: 80px; overflow-y: auto;">
{{selectedLog.agent}}
</div> </div>
</ng-template> </div>
</gui-grid-column>
<gui-grid-column header="Section" field="section"> <h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-code me-2 text-primary"></i>Raw Data</h5>
<ng-template let-value="item.section" let-item="item" let-index="index"> <div class="bg-light p-3 rounded"
{{ value }} style="max-height: 250px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85rem; border: 1px solid #dee2e6;">
</ng-template> {{selectedLog.data}}
</gui-grid-column> </div>
<gui-grid-column header="action" field="action"> </div>
<ng-template let-value="item.action" let-item="item" let-index="index"> </p-drawer>
{{ value }}
</ng-template>
</gui-grid-column>
<gui-grid-column header="ip" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Time" field="created">
<ng-template let-value="item.created" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
</gui-grid>
</c-card-body> </c-card-body>
</c-card> </c-card>

View file

@ -1,18 +1,8 @@
import { Component, OnInit, ViewEncapsulation } from "@angular/core"; import { Component, OnInit, ViewChild, ViewEncapsulation } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router"; import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiRowDetail,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
@ -22,10 +12,11 @@ import { formatInTimeZone } from "date-fns-tz";
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class SyslogComponent implements OnInit { export class SyslogComponent implements OnInit {
public uid: number; public uid!: number;
public uname: string; public uname!: string;
public tz: string= "UTC"; public tz: string= "UTC";
public filterText: string; public filterText!: string;
public userid: number = 0;
public filters: any = { public filters: any = {
start_time: false, start_time: false,
end_time: false, end_time: false,
@ -36,6 +27,10 @@ export class SyslogComponent implements OnInit {
public event_section: any = []; public event_section: any = [];
public event_action: any = []; public event_action: any = [];
public filters_visible: boolean = false; public filters_visible: boolean = false;
public detailsVisible: boolean = false;
public selectedLog: any = null;
@ViewChild('dt') table!: Table;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
private router: Router, private router: Router,
@ -66,95 +61,18 @@ export class SyslogComponent implements OnInit {
} }
} }
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public Selectedrows: any; showLogDetails(log: any) {
public userid: number = 0; this.selectedLog = log;
public sorting = { this.detailsVisible = true;
enabled: true, }
multiSorting: true,
};
public campaignOnestart: any;
public campaignOneend: any;
rowDetail: GuiRowDetail = {
enabled: true,
template: (item) => {
return `
<div class='log-detail' style="width: 355px;color:#fff;background-color:#3399ff">
<h2>System Log :</h2>
<table>
<tr>
<td>Section</td>
<td>${item.section}</td>
</tr>
<tr>
<td>Action</td>
<td>${item.action}</td>
</tr>
<tr>
<td>Time</td>
<td>${item.created}</td>
</tr>
</table>
<h2 style="margin-top: 5px;">User Detail :
</h2>
<table>
<tr>
<td>User</td>
<td>${item.username}</td>
</tr>
<tr>
<td>FirstName</td>
<td>${item.first_name}</td>
</tr>
<tr>
<td>LastName</td>
<td>${item.last_name}</td>
</tr>
<tr>
<td>IP</td>
<td>${item.ip}</td>
</tr>
<tr>
<td>Agent</td>
<td><div style="height: 40px;overflow-y: scroll;">${item.agent}</div></td>
</tr>
</table>
<div class="code-title">data</div>
<code>
${item.data}
</code>
</div>`;
},
};
public paging: GuiPaging = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
page: 1, }
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = { // Removed legacy GuiGrid configs
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
ngOnInit(): void { ngOnInit(): void {
var _self = this; var _self = this;
this.userid = Number(this.route.snapshot.paramMap.get("userid")); this.userid = Number(this.route.snapshot.paramMap.get("userid"));

View file

@ -12,7 +12,9 @@ import {
import { NgxMatSelectSearchModule } from "ngx-mat-select-search"; import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
import { SyslogRoutingModule } from "./syslog-routing.module"; import { SyslogRoutingModule } from "./syslog-routing.module";
import { SyslogComponent } from "./syslog.component"; import { SyslogComponent } from "./syslog.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule } from 'primeng/table';
import { DrawerModule } from 'primeng/drawer';
import { InputTextModule } from 'primeng/inputtext';
import { MatDatepickerModule } from "@angular/material/datepicker"; import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatInputModule } from "@angular/material/input"; import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field"; import { MatFormFieldModule } from "@angular/material/form-field";
@ -28,7 +30,9 @@ import { MatSelectModule } from "@angular/material/select";
GridModule, GridModule,
FormsModule, FormsModule,
ButtonModule, ButtonModule,
GuiGridModule, TableModule,
DrawerModule,
InputTextModule,
CollapseModule, CollapseModule,
DropdownModule, DropdownModule,
MatInputModule, MatInputModule,

View file

@ -4,7 +4,7 @@
<c-card-header> <c-card-header>
<c-row> <c-row>
<c-col xs [lg]="10"> <c-col xs [lg]="10">
Users User Managment
</c-col> </c-col>
<c-col xs [lg]="2" style="text-align: right;"> <c-col xs [lg]="2" style="text-align: right;">
<button cButton color="primary" (click)="editAddUser({},'showadd')"><i <button cButton color="primary" (click)="editAddUser({},'showadd')"><i
@ -12,41 +12,91 @@
</c-col> </c-col>
</c-row> </c-row>
</c-card-header> </c-card-header>
<c-card-body > <c-card-body>
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting" <div class="mb-3 d-flex justify-content-end">
[autoResizeWidth]=true [paging]="paging"> <span class="p-input-icon-left">
<gui-grid-column header="User Name" field="username"> <i class="pi pi-search"></i>
<ng-template let-value="item.username" let-item="item" let-index="index"> <input type="text" pInputText placeholder="Search users..." (input)="applyFilterGlobal($event, 'contains')"
&nbsp; {{value}} </ng-template> class="form-control-sm" />
</gui-grid-column> </span>
<gui-grid-column header="First Name" field="first_name"> </div>
<ng-template let-value="item.first_name" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Last Name" field="last_name">
<ng-template let-value="item.last_name" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Role" field="role">
<ng-template let-value="item.role" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="warning" size="sm" (click)="editAddUser(item,'edit');" ><i
class="fa-regular fa-pen-to-square"></i></button>
<button cButton color="danger" size="sm" class="mx-1" (click)="confirm_delete(item);"><i
class="fa-regular fa-trash-can"></i></button>
<button *ngIf="ispro" cButton color="secondary" size="sm" (click)="showrest(item);"> <p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
<i class="fa-solid fa-fingerprint"></i> [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
</button> [stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
</ng-template> [globalFilterFields]="['username', 'first_name', 'last_name', 'role']" [loading]="loading">
</gui-grid-column>
</gui-grid> <ng-template pTemplate="header">
<tr>
<th pSortableColumn="username" pResizableColumn>
<div class="justify-between">
<span>User Name</span>
<span>
<p-sortIcon field="username"></p-sortIcon>
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="first_name" pResizableColumn>
<div class="justify-between">
<span>First Name</span>
<span>
<p-sortIcon field="first_name"></p-sortIcon>
<p-columnFilter type="text" field="first_name" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="last_name" pResizableColumn>
<div class="justify-between">
<span>Last Name</span>
<span>
<p-sortIcon field="last_name"></p-sortIcon>
<p-columnFilter type="text" field="last_name" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="role" pResizableColumn>
<div class="justify-between">
<span>Role</span>
<span>
<p-sortIcon field="role"></p-sortIcon>
<p-columnFilter type="text" field="role" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>{{item.username}}</td>
<td>{{item.first_name}}</td>
<td>{{item.last_name}}</td>
<td>{{item.role}}</td>
<td class="text-center">
<div class="d-flex gap-1 justify-content-center">
<button cButton color="warning" size="sm" (click)="editAddUser(item,'edit');" pTooltip="Edit User">
<i class="fa-regular fa-pen-to-square"></i>
</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item);" pTooltip="Delete User">
<i class="fa-regular fa-trash-can"></i>
</button>
<button *ngIf="ispro" cButton color="secondary" size="sm" (click)="showrest(item);"
pTooltip="Security Restrictions">
<i class="fa-solid fa-fingerprint"></i>
</button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="5" class="text-center p-4">No users found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-col> </c-col>
@ -54,285 +104,304 @@
<c-modal-header> <c-modal-header>
<c-modal #EditTaskModal backdrop="static" size="lg" [(visible)]="EditTaskModalVisible" id="EditTaskModal"> <c-modal #EditTaskModal backdrop="static" size="lg" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<c-modal-header> <c-modal-header>
<h5 *ngIf="SelectedUser['action']=='edit'" cModalTitle><i class="fa-solid fa-user-pen me-1"></i>Edit: {{SelectedUser['username']}}</h5> <h5 *ngIf="SelectedUser['action']=='edit'" cModalTitle><i class="fa-solid fa-user-pen me-1"></i>Edit:
<h5 *ngIf="SelectedUser['action']=='add'" cModalTitle><i class="fa-solid fa-user-plus me-1"></i>Add User</h5> {{SelectedUser['username']}}</h5>
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button> <h5 *ngIf="SelectedUser['action']=='add'" cModalTitle><i class="fa-solid fa-user-plus me-1"></i>Add User</h5>
</c-modal-header> <button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
<c-modal-body class="p-3"> </c-modal-header>
<!-- User Details --> <c-modal-body class="p-3">
<div class="user-form-section mb-4"> <!-- User Details -->
<div class="section-header mb-3"> <div class="user-form-section mb-4">
<h6 class="section-title mb-1"><i class="fa-solid fa-user me-2"></i>Basic Information</h6> <div class="section-header mb-3">
<small class="text-muted">Enter the user's personal details and login credentials</small> <h6 class="section-title mb-1"><i class="fa-solid fa-user me-2"></i>Basic Information</h6>
</div> <small class="text-muted">Enter the user's personal details and login credentials</small>
<c-row class="g-3">
<c-col xs="12" md="6">
<input cFormControl placeholder="Username (required for login)" [(ngModel)]="SelectedUser['username']"
class="form-input" title="Unique username for system login" />
</c-col>
<c-col xs="12" md="6">
<input cFormControl placeholder="Email Address (for notifications)" [(ngModel)]="SelectedUser['email']"
class="form-input" type="email" title="Valid email address for system notifications" />
</c-col>
<c-col xs="12" md="4">
<input cFormControl placeholder="First Name (optional)" [(ngModel)]="SelectedUser['first_name']"
class="form-input" title="User's first name" />
</c-col>
<c-col xs="12" md="4">
<input cFormControl placeholder="Last Name (optional)" [(ngModel)]="SelectedUser['last_name']"
class="form-input" title="User's last name" />
</c-col>
<c-col xs="12" md="4">
<input type="password" cFormControl placeholder="Password (min 6 characters)" [(ngModel)]="SelectedUser['password']"
class="form-input" title="Secure password for user login" />
</c-col>
</c-row>
</div>
<!-- System Permissions -->
<div class="permissions-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-shield-halved me-2"></i>System Access Levels</h6>
<small class="text-muted">Control what system features this user can access and modify</small>
</div>
<div class="permission-legend mb-2">
<span class="legend-item"><span class="legend-color bg-info"></span>R = Read Only</span>
<span class="legend-item"><span class="legend-color bg-warning"></span>W = Read & Write</span>
<span class="legend-item"><span class="legend-color bg-success"></span>F = Full Control</span>
<span class="legend-item"><span class="legend-color bg-secondary"></span>× = No Access</span>
</div>
<div class="permissions-compact">
<div *ngFor='let perm of adminperms | keyvalue' class="perm-row">
<span class="perm-label">{{ perm.key.replace('_', ' ') }}</span>
<div class="perm-controls">
<button cButton size="sm" variant="outline" color="info" [active]="adminperms[perm.key]=='read'"
(click)="setRadioValue(perm.key,'read')" title="Read Only Access">R</button>
<button cButton size="sm" variant="outline" color="warning" [active]="adminperms[perm.key]=='write'"
(click)="setRadioValue(perm.key,'write')" title="Read & Write Access">W</button>
<button cButton size="sm" variant="outline" color="success" [active]="adminperms[perm.key]=='full'"
(click)="setRadioValue(perm.key,'full')" title="Full Control Access">F</button>
<button cButton size="sm" variant="outline" color="secondary" [active]="adminperms[perm.key]=='none'"
(click)="setRadioValue(perm.key,'none')" title="No Access">×</button>
</div>
</div> </div>
</div> <c-row class="g-3">
</div> <c-col xs="12" md="6">
<input cFormControl placeholder="Username (required for login)" [(ngModel)]="SelectedUser['username']"
<!-- Device Permissions --> class="form-input" title="Unique username for system login" />
<div class="device-permissions-section">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-network-wired me-2"></i>Device Access Control</h6>
<small class="text-muted">Grant access to specific device groups with defined permission levels</small>
</div>
<!-- Add New Permission -->
<div class="add-permission-card mb-3">
<div class="add-header">
<i class="fa-solid fa-plus-circle text-primary me-2"></i>
<span class="fw-semibold">Add Device Permission</span>
</div>
<c-row class="g-2 mt-2">
<c-col xs="12" md="5">
<div class="search-select-wrapper">
<label class="search-label">Device Group</label>
<input cFormControl
[(ngModel)]="devgroupSearch"
(input)="filterDevGroups($event)"
(focus)="showDevGroupDropdown = true"
(blur)="hideDevGroupDropdown()"
placeholder="Search and select device group..."
class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length > 0" class="search-dropdown">
<div *ngFor="let group of filteredDevGroups"
class="search-option"
(mousedown)="selectDevGroup(group)">
{{group.name}}
</div>
</div>
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length === 0 && devgroupSearch" class="search-no-results">
No groups found
</div>
</div>
</c-col> </c-col>
<c-col xs="12" md="5"> <c-col xs="12" md="6">
<div class="search-select-wrapper"> <input cFormControl placeholder="Email Address (for notifications)" [(ngModel)]="SelectedUser['email']"
<label class="search-label">Permission Level</label> class="form-input" type="email" title="Valid email address for system notifications" />
<input cFormControl
[(ngModel)]="permissionSearch"
(input)="filterPermissions($event)"
(focus)="showPermissionDropdown = true"
(blur)="hidePermissionDropdown()"
placeholder="Search and select permission..."
class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
<div *ngFor="let perm of filteredPermissions"
class="search-option"
(mousedown)="selectPermission(perm)">
{{perm.name}}
</div>
</div>
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch" class="search-no-results">
No permissions found
</div>
</div>
</c-col> </c-col>
<c-col xs="12" md="2" class="d-flex align-items-end"> <c-col xs="12" md="4">
<button *ngIf="SelectedUser['action']=='edit'" cButton color="success" size="sm" class="w-100 add-btn" <input cFormControl placeholder="First Name (optional)" [(ngModel)]="SelectedUser['first_name']"
(click)="add_user_perm()" [disabled]="!devgroup || !permission"> class="form-input" title="User's first name" />
<i class="fa-solid fa-plus me-1"></i>Add Access </c-col>
</button> <c-col xs="12" md="4">
<button *ngIf="SelectedUser['action']=='add'" cButton color="success" size="sm" class="w-100 add-btn" <input cFormControl placeholder="Last Name (optional)" [(ngModel)]="SelectedUser['last_name']"
(click)="add_new_user_perm()" [disabled]="!devgroup || !permission"> class="form-input" title="User's last name" />
<i class="fa-solid fa-plus me-1"></i>Add Access </c-col>
</button> <c-col xs="12" md="4">
<input type="password" cFormControl placeholder="Password (min 6 characters)"
[(ngModel)]="SelectedUser['password']" class="form-input" title="Secure password for user login" />
</c-col> </c-col>
</c-row> </c-row>
</div> </div>
<!-- Current Permissions --> <!-- System Permissions -->
<div class="current-permissions"> <div class="permissions-section mb-4">
<div class="permissions-header"> <div class="section-header mb-3">
<h6 class="mb-1"><i class="fa-solid fa-list-check me-2 text-success"></i>Current Device Access</h6> <h6 class="section-title mb-1"><i class="fa-solid fa-shield-halved me-2"></i>System Access Levels</h6>
<span class="badge bg-info">{{userperms.length}} permission(s)</span> <small class="text-muted">Control what system features this user can access and modify</small>
</div> </div>
<div class="permission-legend mb-2">
<div *ngIf="userperms.length>0" class="permissions-list mt-2"> <span class="legend-item"><span class="legend-color bg-info"></span>R = Read Only</span>
<div *ngFor="let perm of userperms; let i = index" class="permission-item"> <span class="legend-item"><span class="legend-color bg-warning"></span>W = Read & Write</span>
<div class="perm-number">{{i + 1}}</div> <span class="legend-item"><span class="legend-color bg-success"></span>F = Full Control</span>
<div class="perm-info"> <span class="legend-item"><span class="legend-color bg-secondary"></span>× = No Access</span>
<div class="perm-main"> </div>
<i class="fa-solid fa-server me-1 text-primary"></i> <div class="permissions-compact">
<span class="group-name">{{perm.group_name}}</span> <div *ngFor='let perm of adminperms | keyvalue' class="perm-row">
</div> <span class="perm-label">{{ perm.key.replace('_', ' ') }}</span>
<div class="perm-detail"> <div class="perm-controls">
<i class="fa-solid fa-key me-1 text-warning"></i> <button cButton size="sm" variant="outline" color="info" [active]="adminperms[perm.key]=='read'"
<span class="perm-name">{{perm.perm_name}}</span> (click)="setRadioValue(perm.key,'read')" title="Read Only Access">R</button>
</div> <button cButton size="sm" variant="outline" color="warning" [active]="adminperms[perm.key]=='write'"
(click)="setRadioValue(perm.key,'write')" title="Read & Write Access">W</button>
<button cButton size="sm" variant="outline" color="success" [active]="adminperms[perm.key]=='full'"
(click)="setRadioValue(perm.key,'full')" title="Full Control Access">F</button>
<button cButton size="sm" variant="outline" color="secondary" [active]="adminperms[perm.key]=='none'"
(click)="setRadioValue(perm.key,'none')" title="No Access">×</button>
</div> </div>
<button cButton color="danger" size="sm" variant="outline" (click)="confirm_delete_perm(perm)"
title="Remove this permission" class="remove-btn">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</div>
<div *ngIf="userperms.length==0" class="empty-permissions">
<div class="empty-icon">
<i class="fa-solid fa-shield-xmark text-muted"></i>
</div>
<div class="empty-text">
<strong>No device access granted</strong>
<p class="mb-0 text-muted">Add permissions above to grant access to device groups</p>
</div> </div>
</div> </div>
</div> </div>
</div>
</c-modal-body>
<c-modal-footer class="d-flex justify-content-between flex-wrap gap-2 p-2">
<div>
<button *ngIf="SelectedUser['role']!='disabled'" (click)="SelectedUser['role']='disabled'" cButton color="danger" size="sm">
<i class="fa-solid fa-user-slash me-1"></i><span class="d-none d-sm-inline">Deactivate</span>
</button>
<button *ngIf="SelectedUser['role']=='disabled'" (click)="SelectedUser['role']='admin'" cButton color="success" size="sm">
<i class="fa-solid fa-user-check me-1"></i><span class="d-none d-sm-inline">Activate</span>
</button>
</div>
<div class="d-flex gap-2">
<button *ngIf="SelectedUser['action']=='add'" (click)="submit('add')" cButton color="primary" size="sm">
<i class="fa-solid fa-plus me-1"></i>Add
</button>
<button *ngIf="SelectedUser['action']=='edit'" (click)="submit('edit')" cButton color="primary" size="sm">
<i class="fa-solid fa-save me-1"></i>Save
</button>
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" size="sm">
<i class="fa-solid fa-times me-1"></i>Close
</button>
</div>
</c-modal-footer>
</c-modal>
<c-modal #RestrictionsTaskModal *ngIf="ispro && userresttrictions" backdrop="static" size="lg" [(visible)]="RestrictionsTaskModalVisible" id="RestrictionsTaskModal"> <!-- Device Permissions -->
<c-modal-header> <div class="device-permissions-section">
<h5 cModalTitle>Security Restrictions of {{SelectedUser['username']}}</h5> <div class="section-header mb-3">
</c-modal-header> <h6 class="section-title mb-1"><i class="fa-solid fa-network-wired me-2"></i>Device Access Control</h6>
<c-modal-body> <small class="text-muted">Grant access to specific device groups with defined permission levels</small>
<table width="100%">
<tr>
<td><h6>TOTP status :</h6></td>
<td>
<c-form-check sizing="xl" switch>
<input cFormCheckInput [(ngModel)]="userresttrictions['totp']" [checked]="userresttrictions['totp']" type="checkbox" />
<label *ngIf="userresttrictions['totp']" cFormCheckLabel> TOTP is active</label>
<label *ngIf="!userresttrictions['totp']" cFormCheckLabel> TOTP is deactive</label>
</c-form-check>
</td>
</tr>
<tr>
<td><h6>Use OTP for device login:</h6></td>
<td>
<c-button-group aria-label="Basic example" role="group">
<button cButton color="info" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='system'"
(click)="userresttrictions['device-totp']='system'">System Defined</button>
<button cButton color="danger" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='yes'"
(click)="userresttrictions['device-totp']='yes'">TOTP</button>
<button cButton color="success" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='no'"
(click)="userresttrictions['device-totp']='no'">Password</button>
</c-button-group>
</td>
</tr>
<tr>
<td><h6>Restrict IP access:</h6></td>
<td>
<c-form-check sizing="xl" switch>
<input cFormCheckInput [(ngModel)]="userresttrictions['ip']" [checked]="userresttrictions['ip']" type="checkbox" />
<label *ngIf="userresttrictions['ip']" cFormCheckLabel> Restricted</label>
<label *ngIf="!userresttrictions['ip']" cFormCheckLabel> Not Restricted</label>
</c-form-check>
</td>
</tr>
</table>
<c-input-group *ngIf="userresttrictions['ip'] && userresttrictions['allowed_ips'].length>0" class="mb-3">
<h5>Allowed ips :</h5>
<gui-grid [autoResizeWidth]="true" [source]="userresttrictions['allowed_ips']" [columnMenu]="columnMenu" [sorting]="sorting"
[autoResizeWidth]=true [paging]="paging" >
<gui-grid-column header="IP Address" >
<ng-template let-value="item" let-item="item" let-index="index">
&nbsp; {{item}} </ng-template>
</gui-grid-column>
<gui-grid-column header="Action" width="80" align="center">
<ng-template let-value="item" let-item="item" let-index="index">
<button cButton color="danger" (click)="delete_ip(item)"><i class="fa-regular fa-trash-can"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
</c-input-group>
<hr />
<table *ngIf="userresttrictions['ip']" class="mb-3">
<td style="width: 30%;">
<span>Add new IP</span>
</td>
<td>
<div >
<input cFormControl id="floatingInput" placeholder="IP address/cidr" [(ngModel)]="ipaddress" />
</div> </div>
</td>
<td style="vertical-align: top;"> <!-- Add New Permission -->
<button cButton color="primary" (click)="add_ip()">Add+</button> <div class="add-permission-card mb-3">
</td> <div class="add-header">
</table> <i class="fa-solid fa-plus-circle text-primary me-2"></i>
</c-modal-body> <span class="fw-semibold">Add Device Permission</span>
<c-modal-footer> </div>
<button (click)="save_sec()" cButton color="primary">Save</button> <c-row class="g-2 mt-2">
<button [cModalToggle]="RestrictionsTaskModal.id" cButton color="secondary"> <c-col xs="12" md="5">
Close <div class="search-select-wrapper">
</button> <label class="search-label">Device Group</label>
</c-modal-footer> <input cFormControl [(ngModel)]="devgroupSearch" (input)="filterDevGroups($event)"
</c-modal> (focus)="showDevGroupDropdown = true" (blur)="hideDevGroupDropdown()"
placeholder="Search and select device group..." class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length > 0" class="search-dropdown">
<div *ngFor="let group of filteredDevGroups" class="search-option"
(mousedown)="selectDevGroup(group)">
{{group.name}}
</div>
</div>
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length === 0 && devgroupSearch"
class="search-no-results">
No groups found
</div>
</div>
</c-col>
<c-col xs="12" md="5">
<div class="search-select-wrapper">
<label class="search-label">Permission Level</label>
<input cFormControl [(ngModel)]="permissionSearch" (input)="filterPermissions($event)"
(focus)="showPermissionDropdown = true" (blur)="hidePermissionDropdown()"
placeholder="Search and select permission..." class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
<div *ngFor="let perm of filteredPermissions" class="search-option"
(mousedown)="selectPermission(perm)">
{{perm.name}}
</div>
</div>
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch"
class="search-no-results">
No permissions found
</div>
</div>
</c-col>
<c-col xs="12" md="2" class="d-flex align-items-end">
<button *ngIf="SelectedUser['action']=='edit'" cButton color="success" size="sm" class="w-100 add-btn"
(click)="add_user_perm()" [disabled]="!devgroup || !permission">
<i class="fa-solid fa-plus me-1"></i>Add Access
</button>
<button *ngIf="SelectedUser['action']=='add'" cButton color="success" size="sm" class="w-100 add-btn"
(click)="add_new_user_perm()" [disabled]="!devgroup || !permission">
<i class="fa-solid fa-plus me-1"></i>Add Access
</button>
</c-col>
</c-row>
</div>
<!-- Current Permissions -->
<div class="current-permissions">
<div class="permissions-header">
<h6 class="mb-1"><i class="fa-solid fa-list-check me-2 text-success"></i>Current Device Access</h6>
<span class="badge bg-info">{{userperms.length}} permission(s)</span>
</div>
<div *ngIf="userperms.length>0" class="permissions-list mt-2">
<div *ngFor="let perm of userperms; let i = index" class="permission-item">
<div class="perm-number">{{i + 1}}</div>
<div class="perm-info">
<div class="perm-main">
<i class="fa-solid fa-server me-1 text-primary"></i>
<span class="group-name">{{perm.group_name}}</span>
</div>
<div class="perm-detail">
<i class="fa-solid fa-key me-1 text-warning"></i>
<span class="perm-name">{{perm.perm_name}}</span>
</div>
</div>
<button cButton color="danger" size="sm" variant="outline" (click)="confirm_delete_perm(perm)"
title="Remove this permission" class="remove-btn">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</div>
<div *ngIf="userperms.length==0" class="empty-permissions">
<div class="empty-icon">
<i class="fa-solid fa-shield-xmark text-muted"></i>
</div>
<div class="empty-text">
<strong>No device access granted</strong>
<p class="mb-0 text-muted">Add permissions above to grant access to device groups</p>
</div>
</div>
</div>
</div>
</c-modal-body>
<c-modal-footer class="d-flex justify-content-between flex-wrap gap-2 p-2">
<div>
<button *ngIf="SelectedUser['role']!='disabled'" (click)="SelectedUser['role']='disabled'" cButton
color="danger" size="sm">
<i class="fa-solid fa-user-slash me-1"></i><span class="d-none d-sm-inline">Deactivate</span>
</button>
<button *ngIf="SelectedUser['role']=='disabled'" (click)="SelectedUser['role']='admin'" cButton color="success"
size="sm">
<i class="fa-solid fa-user-check me-1"></i><span class="d-none d-sm-inline">Activate</span>
</button>
</div>
<div class="d-flex gap-2">
<button *ngIf="SelectedUser['action']=='add'" (click)="submit('add')" cButton color="primary" size="sm">
<i class="fa-solid fa-plus me-1"></i>Add
</button>
<button *ngIf="SelectedUser['action']=='edit'" (click)="submit('edit')" cButton color="primary" size="sm">
<i class="fa-solid fa-save me-1"></i>Save
</button>
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" size="sm">
<i class="fa-solid fa-times me-1"></i>Close
</button>
</div>
</c-modal-footer>
</c-modal>
<c-modal #RestrictionsTaskModal *ngIf="ispro && userresttrictions" backdrop="static" size="lg"
[(visible)]="RestrictionsTaskModalVisible" id="RestrictionsTaskModal">
<c-modal-header>
<h5 cModalTitle>Security Restrictions of {{SelectedUser['username']}}</h5>
</c-modal-header>
<c-modal-body>
<table width="100%">
<tr>
<td>
<h6>TOTP status :</h6>
</td>
<td>
<c-form-check sizing="xl" switch>
<input cFormCheckInput [(ngModel)]="userresttrictions['totp']" [checked]="userresttrictions['totp']"
type="checkbox" />
<label *ngIf="userresttrictions['totp']" cFormCheckLabel> TOTP is active</label>
<label *ngIf="!userresttrictions['totp']" cFormCheckLabel> TOTP is deactive</label>
</c-form-check>
</td>
</tr>
<tr>
<td>
<h6>Use OTP for device login:</h6>
</td>
<td>
<c-button-group aria-label="Basic example" role="group">
<button cButton color="info" variant="outline" size="sm"
[active]="userresttrictions['device-totp']=='system'"
(click)="userresttrictions['device-totp']='system'">System Defined</button>
<button cButton color="danger" variant="outline" size="sm"
[active]="userresttrictions['device-totp']=='yes'"
(click)="userresttrictions['device-totp']='yes'">TOTP</button>
<button cButton color="success" variant="outline" size="sm"
[active]="userresttrictions['device-totp']=='no'"
(click)="userresttrictions['device-totp']='no'">Password</button>
</c-button-group>
</td>
</tr>
<tr>
<td>
<h6>Restrict IP access:</h6>
</td>
<td>
<c-form-check sizing="xl" switch>
<input cFormCheckInput [(ngModel)]="userresttrictions['ip']" [checked]="userresttrictions['ip']"
type="checkbox" />
<label *ngIf="userresttrictions['ip']" cFormCheckLabel> Restricted</label>
<label *ngIf="!userresttrictions['ip']" cFormCheckLabel> Not Restricted</label>
</c-form-check>
</td>
</tr>
</table>
<c-input-group *ngIf="userresttrictions['ip'] && userresttrictions['allowed_ips'].length>0" class="mb-3">
<h5>Allowed ips :</h5>
<p-table [value]="userresttrictions['allowed_ips']" [paginator]="true" [rows]="5" [showGridlines]="true"
[stripedRows]="true" styleClass="p-datatable-sm" class="w-100">
<ng-template pTemplate="header">
<tr>
<th>IP Address</th>
<th style="width: 80px" class="text-center">Action</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>{{item}}</td>
<td class="text-center">
<button cButton color="danger" size="sm" (click)="delete_ip(item)" pTooltip="Remove IP">
<i class="fa-regular fa-trash-can"></i>
</button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="2" class="text-center p-2">No restricted IPs.</td>
</tr>
</ng-template>
</p-table>
</c-input-group>
<hr />
<table *ngIf="userresttrictions['ip']" class="mb-3">
<td style="width: 30%;">
<span>Add new IP</span>
</td>
<td>
<div>
<input cFormControl id="floatingInput" placeholder="IP address/cidr" [(ngModel)]="ipaddress" />
</div>
</td>
<td style="vertical-align: top;">
<button cButton color="primary" (click)="add_ip()">Add+</button>
</td>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="save_sec()" cButton color="primary">Save</button>
<button [cModalToggle]="RestrictionsTaskModal.id" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>
@ -376,7 +445,8 @@
</c-modal-footer> </c-modal-footer>
</c-modal> </c-modal>
<c-modal #DeletePermConfirmModal backdrop="static" [(visible)]="DeletePermConfirmModalVisible" id="DeletePermConfirmModal"> <c-modal #DeletePermConfirmModal backdrop="static" [(visible)]="DeletePermConfirmModalVisible"
id="DeletePermConfirmModal">
<c-modal-header> <c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedUser['name'] }}</h5> <h5 cModalTitle>Confirm delete {{ SelectedUser['name'] }}</h5>
<button [cModalToggle]="DeletePermConfirmModal.id" cButtonClose></button> <button [cModalToggle]="DeletePermConfirmModal.id" cButtonClose></button>

View file

@ -1,17 +1,8 @@
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core"; import { Component, OnInit, QueryList, ViewChildren, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiGridComponent,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { NgxSuperSelectOptions } from "ngx-super-select"; import { NgxSuperSelectOptions } from "ngx-super-select";
import { AppToastComponent } from "../toast-simple/toast.component"; import { AppToastComponent } from "../toast-simple/toast.component";
import { ToasterComponent } from "@coreui/angular"; import { ToasterComponent } from "@coreui/angular";
@ -21,11 +12,11 @@ import { ToasterComponent } from "@coreui/angular";
styleUrls: ["user_manager.scss"], styleUrls: ["user_manager.scss"],
}) })
export class UserManagerComponent implements OnInit { export class UserManagerComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public ispro:boolean=false; public ispro: boolean = false;
gridComponent: GuiGridComponent; @ViewChild('dt') table!: Table;
toasterForm = { toasterForm = {
autohide: true, autohide: true,
delay: 10000, delay: 10000,
@ -64,7 +55,6 @@ export class UserManagerComponent implements OnInit {
} }
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = false; public loading: boolean = false;
public rows: any = []; public rows: any = [];
public SelectedUser: any = {}; public SelectedUser: any = {};
@ -87,8 +77,8 @@ export class UserManagerComponent implements OnInit {
public DeletePermConfirmModalVisible: boolean = false; public DeletePermConfirmModalVisible: boolean = false;
public userperms: any = {}; public userperms: any = {};
public userresttrictions: any = false; public userresttrictions: any = false;
public ipaddress:string=""; public ipaddress: string = "";
public adminperms: { [index: string]: string }; public adminperms: { [index: string]: string } = {};
public defadminperms: { [index: string]: string } = { public defadminperms: { [index: string]: string } = {
device: "none", device: "none",
device_group: "none", device_group: "none",
@ -103,11 +93,6 @@ export class UserManagerComponent implements OnInit {
system_backup: "none", system_backup: "none",
}; };
public sorting = {
enabled: true,
multiSorting: true,
};
options: Partial<NgxSuperSelectOptions> = { options: Partial<NgxSuperSelectOptions> = {
actionsEnabled: false, actionsEnabled: false,
displayExpr: "name", displayExpr: "name",
@ -117,20 +102,6 @@ export class UserManagerComponent implements OnInit {
enableDarkMode: false, enableDarkMode: false,
}; };
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
setRadioValue(key: string, value: string): void { setRadioValue(key: string, value: string): void {
this.adminperms[key] = value; this.adminperms[key] = value;
} }
@ -169,11 +140,9 @@ export class UserManagerComponent implements OnInit {
setTimeout(() => this.showPermissionDropdown = false, 200); setTimeout(() => this.showPermissionDropdown = false, 200);
} }
public rowSelection: boolean | GuiRowSelection = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
type: GuiRowSelectionType.CHECKBOX, }
mode: GuiRowSelectionMode.MULTIPLE,
};
ngOnInit(): void { ngOnInit(): void {
this.initGridTable(); this.initGridTable();
@ -188,12 +157,12 @@ export class UserManagerComponent implements OnInit {
); );
componentRef.instance["closeButton"] = props.closeButton; componentRef.instance["closeButton"] = props.closeButton;
} }
totp(item:any){ totp(item: any) {
this.SelectedUser = item; this.SelectedUser = item;
this.data_provider.totp('enable',this.SelectedUser.id).then((res) => { this.data_provider.totp('enable', this.SelectedUser.id).then((res) => {
if(res.status == "success"){ if (res.status == "success") {
this.show_toast("Success", "Totp generated successfully", "success"); this.show_toast("Success", "Totp generated successfully", "success");
}else{ } else {
this.show_toast("Error", res.err, "danger"); this.show_toast("Error", res.err, "danger");
} }
}); });
@ -218,15 +187,15 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else {
if ("id" in res && !("status" in res)) { if ("id" in res && !("status" in res)) {
_self.initGridTable(); _self.initGridTable();
this.EditTaskModalVisible = false; this.EditTaskModalVisible = false;
} else { } else {
//show error //show error
_self.show_toast("Error", res.err, "danger"); _self.show_toast("Error", res.err, "danger");
}
} }
}
}); });
} else { } else {
if (_self.userperms.length > 0) { if (_self.userperms.length > 0) {
@ -243,9 +212,9 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else {
_self.initGridTable(); _self.initGridTable();
_self.EditTaskModalVisible = false; _self.EditTaskModalVisible = false;
} }
}); });
} }
@ -262,27 +231,30 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else if ("err" in res) {
_self.allPerms = res.map((x: any) => { _self.show_toast("Error", res.err, "danger");
return { id: x["id"], name: x.name }; }
}); else {
_self.filteredPermissions = [..._self.allPerms]; _self.allPerms = res.map((x: any) => {
_self.data_provider.get_devgroup_list().then((res) => {
if ("error" in res && res.error.indexOf("Unauthorized")) {
_self.show_toast(
"Error",
"You are not authorized to perform this action",
"danger"
);
}
else{
_self.allDevGroups = res.map((x: any) => {
return { id: x["id"], name: x.name }; return { id: x["id"], name: x.name };
}); });
_self.filteredDevGroups = [..._self.allDevGroups]; _self.filteredPermissions = [..._self.allPerms];
_self.data_provider.get_devgroup_list().then((res) => {
if ("error" in res && res.error.indexOf("Unauthorized")) {
_self.show_toast(
"Error",
"You are not authorized to perform this action",
"danger"
);
}
else {
_self.allDevGroups = res.map((x: any) => {
return { id: x["id"], name: x.name };
});
_self.filteredDevGroups = [..._self.allDevGroups];
}
});
} }
});
}
}); });
if (action == "showadd") { if (action == "showadd") {
this.userperms = []; this.userperms = [];
@ -303,6 +275,10 @@ export class UserManagerComponent implements OnInit {
this.EditTaskModalVisible = true; this.EditTaskModalVisible = true;
return; return;
} }
if (item.username == "system") {
this.show_toast("Error", "System user cannot be edited", "danger");
return;
}
this.SelectedUser = { ...item }; this.SelectedUser = { ...item };
if (this.SelectedUser["adminperms"].length > 0) { if (this.SelectedUser["adminperms"].length > 0) {
this.adminperms = JSON.parse(this.SelectedUser["adminperms"]); this.adminperms = JSON.parse(this.SelectedUser["adminperms"]);
@ -316,13 +292,13 @@ export class UserManagerComponent implements OnInit {
_self.EditTaskModalVisible = true; _self.EditTaskModalVisible = true;
} }
checkIpAddress(ip:string) { checkIpAddress(ip: string) {
const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|\/|)){4}\b(0?[1-9]|1[0-9]|2[0-9]|3[0-2])\b$/; const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|\/|)){4}\b(0?[1-9]|1[0-9]|2[0-9]|3[0-2])\b$/;
return ipv4Pattern.test(ip) return ipv4Pattern.test(ip)
} }
showrest(item: any) { showrest(item: any) {
var _self=this; var _self = this;
this.SelectedUser = { ...item }; this.SelectedUser = { ...item };
this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => { this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => {
@ -330,33 +306,33 @@ export class UserManagerComponent implements OnInit {
_self.RestrictionsTaskModalVisible = true; _self.RestrictionsTaskModalVisible = true;
}); });
} }
delete_ip(item:string){ delete_ip(item: string) {
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!=item); this.userresttrictions['allowed_ips'] = this.userresttrictions['allowed_ips'].filter((x: any) => x != item);
} }
add_ip(){ add_ip() {
//check if ip address is valid cidr and not added before //check if ip address is valid cidr and not added before
let ip=this.ipaddress.trim(); let ip = this.ipaddress.trim();
if(ip=="")return; if (ip == "") return;
if(this.userresttrictions['allowed_ips'].includes(ip)){ if (this.userresttrictions['allowed_ips'].includes(ip)) {
this.show_toast("Error", "IP already added", "danger"); this.show_toast("Error", "IP already added", "danger");
return; return;
} }
//check if ip is valid cidr ip //check if ip is valid cidr ip
if(this.checkIpAddress(ip)){ if (this.checkIpAddress(ip)) {
this.userresttrictions['allowed_ips'].push(ip); this.userresttrictions['allowed_ips'].push(ip);
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!=""); this.userresttrictions['allowed_ips'] = this.userresttrictions['allowed_ips'].filter((x: any) => x != "");
this.ipaddress=""; this.ipaddress = "";
} }
else{ else {
this.show_toast("Error", "Invalid IP address", "danger"); this.show_toast("Error", "Invalid IP address", "danger");
} }
} }
save_sec(){ save_sec() {
var _self=this; var _self = this;
this.data_provider.save_user_restrictions(this.SelectedUser.id,this.userresttrictions).then((res) => { this.data_provider.save_user_restrictions(this.SelectedUser.id, this.userresttrictions).then((res) => {
if ("error" in res && res.error.indexOf("Unauthorized")) { if ("error" in res && res.error.indexOf("Unauthorized")) {
_self.show_toast( _self.show_toast(
"Error", "Error",
@ -364,12 +340,12 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else {
if('status' in res && res['status']=='success') if ('status' in res && res['status'] == 'success')
this.RestrictionsTaskModalVisible = false; this.RestrictionsTaskModalVisible = false;
else if('status' in res && res['status']=='failed') else if ('status' in res && res['status'] == 'failed')
this.show_toast("Error", res.err, "danger"); this.show_toast("Error", res.err, "danger");
else else
this.show_toast("Error", "Somthing went wrong", "danger"); this.show_toast("Error", "Somthing went wrong", "danger");
} }
}); });
@ -391,10 +367,10 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else {
_self.get_user_perms(_self.SelectedUser["id"]); _self.get_user_perms(_self.SelectedUser["id"]);
_self.permission = 0; _self.permission = 0;
_self.devgroup = 0; _self.devgroup = 0;
} }
}); });
} }
@ -425,9 +401,12 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else if ("err" in res) {
_self.initGridTable(); _self.show_toast("Error", res.err, "danger");
_self.DeleteConfirmModalVisible = false; }
else {
_self.initGridTable();
_self.DeleteConfirmModalVisible = false;
} }
}); });
} }
@ -451,8 +430,8 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else {
this.get_user_perms(this.SelectedUser["id"]); this.get_user_perms(this.SelectedUser["id"]);
} }
}); });
} }
@ -474,13 +453,13 @@ export class UserManagerComponent implements OnInit {
"danger" "danger"
); );
} }
else{ else {
_self.source = res.map((x: any) => { _self.source = res.map((x: any) => {
return x; return x;
}); });
_self.SelectedUser = {}; _self.SelectedUser = {};
_self.loading = false; _self.loading = false;
} }
}); });
} }
} }

View file

@ -16,7 +16,9 @@ import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
import { UserManagerRoutingModule } from "./user_manager-routing.module"; import { UserManagerRoutingModule } from "./user_manager-routing.module";
import { UserManagerComponent } from "./user_manager.component"; import { UserManagerComponent } from "./user_manager.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule } from 'primeng/table';
import { InputTextModule } from 'primeng/inputtext';
import { TooltipModule } from 'primeng/tooltip';
@NgModule({ @NgModule({
imports: [ imports: [
@ -29,7 +31,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, TableModule,
InputTextModule,
TooltipModule,
ModalModule, ModalModule,
FormsModule, FormsModule,
ToastModule, ToastModule,

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,8 @@
import { Component, OnInit, OnDestroy, QueryList, ViewChildren } from "@angular/core"; import { Component, OnInit, OnDestroy, QueryList, ViewChildren, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { NgxSuperSelectOptions } from "ngx-super-select"; import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
import { AppToastComponent } from "../toast-simple/toast.component"; import { AppToastComponent } from "../toast-simple/toast.component";
@ -25,10 +14,13 @@ import { ToasterComponent } from "@coreui/angular";
styleUrls: ["user_tasks.component.scss"] styleUrls: ["user_tasks.component.scss"]
}) })
export class UserTasksComponent implements OnInit { export class UserTasksComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public ispro: boolean = false; public ispro: boolean = false;
@ViewChild('dt') table!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>; @ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
toasterForm = { toasterForm = {
autohide: true, autohide: true,
@ -67,7 +59,6 @@ export class UserTasksComponent implements OnInit {
} }
} }
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public SelectedTask: any = {}; public SelectedTask: any = {};
@ -77,6 +68,7 @@ export class UserTasksComponent implements OnInit {
public DeleteConfirmModalVisible: boolean = false; public DeleteConfirmModalVisible: boolean = false;
public Members: any = ""; public Members: any = "";
public Snippets: any; public Snippets: any;
public Sequences: any = [];
public SelectedMembers: any = []; public SelectedMembers: any = [];
public NewMemberModalVisible: boolean = false; public NewMemberModalVisible: boolean = false;
public availbleMembers: any = []; public availbleMembers: any = [];
@ -137,14 +129,13 @@ export class UserTasksComponent implements OnInit {
public cronSearch: string = ''; public cronSearch: string = '';
public selectedCronPreset: any = null; public selectedCronPreset: any = null;
public filteredCrons: any[] = []; public filteredCrons: any[] = [];
public sorting = { applyFilterGlobal($event: any, stringVal: string) {
enabled: true, this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
multiSorting: true, }
};
searching: GuiSearching = { applyFilterNewMembers($event: any, stringVal: string) {
enabled: true, this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
placeholder: "Search Devices", }
};
options: Partial<NgxSuperSelectOptions> = { options: Partial<NgxSuperSelectOptions> = {
selectionMode: "single", selectionMode: "single",
@ -156,32 +147,16 @@ export class UserTasksComponent implements OnInit {
enableDarkMode: false, enableDarkMode: false,
}; };
public paging: GuiPaging = { seqOptions: Partial<NgxSuperSelectOptions> = {
enabled: true, selectionMode: "single",
page: 1, actionsEnabled: false,
pageSize: 10, displayExpr: "name",
pageSizes: [5, 10, 25, 50], valueExpr: "id",
display: GuiPagingDisplay.ADVANCED, placeholder: "Sequence",
searchEnabled: true,
enableDarkMode: false,
}; };
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
show_new_member_form() { show_new_member_form() {
this.NewMemberModalVisible = true; this.NewMemberModalVisible = true;
@ -256,9 +231,9 @@ export class UserTasksComponent implements OnInit {
} }
} }
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void { onSelectedRowsNewMembers(rows: any[]): void {
this.NewMemberRows = rows; this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source); this.SelectedNewMemberRows = rows;
} }
add_new_members() { add_new_members() {
@ -305,9 +280,11 @@ export class UserTasksComponent implements OnInit {
const currentCron = this.SelectedTask['cron']; const currentCron = this.SelectedTask['cron'];
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) { if ((this.SelectedTask['task_type'] == 'firmware' || this.SelectedTask['task_type'] == 'sequence') && 'data' in this.SelectedTask && this.SelectedTask['data']) {
this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']); if (typeof this.SelectedTask['data'] === 'string') {
if (this.SelectedTask['data']['strategy'] == 'defined') { this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']);
}
if (this.SelectedTask['task_type'] == 'firmware' && this.SelectedTask['data']['strategy'] == 'defined') {
this.data_provider.get_firms(0, 10000, false).then((res) => { this.data_provider.get_firms(0, 10000, false).then((res) => {
let index = 1; let index = 1;
_self.available_firmwares = [ _self.available_firmwares = [
@ -328,16 +305,23 @@ export class UserTasksComponent implements OnInit {
_self.firms_loaded = true; _self.firms_loaded = true;
}); });
} }
else{ else {
_self.firms_loaded = true; _self.firms_loaded = true;
} }
} }
_self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { _self.data_provider.get_snippets("", "", "", 0, 1000, false).then((res) => {
_self.Snippets = res.map((x: any) => { _self.Snippets = res.map((x: any) => {
return { id: x.id, name: x.name }; return { id: x.id, name: x.name };
}); });
}); });
if (_self.ispro) {
_self.data_provider.get_sequences().then((res: any) => {
_self.Sequences = res.map((x: any) => {
return { id: x.id, name: x.name };
});
});
}
if (action != "select_change") { if (action != "select_change") {
this.SelectedTask["action"] = "edit"; this.SelectedTask["action"] = "edit";
this.data_provider.get_task_members(_self.SelectedTask.id).then((res) => { this.data_provider.get_task_members(_self.SelectedTask.id).then((res) => {
@ -405,7 +389,7 @@ export class UserTasksComponent implements OnInit {
onSnippetsValueChanged(v: any) { onSnippetsValueChanged(v: any) {
var _self = this; var _self = this;
if (v == "" || v.length < 3) return; if (v == "" || v.length < 3) return;
_self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => { _self.data_provider.get_snippets(v, "", "", 0, 1000, false).then((res) => {
_self.Snippets = res.map((x: any) => { _self.Snippets = res.map((x: any) => {
return { id: String(x.id), name: x.name }; return { id: String(x.id), name: x.name };
}); });
@ -522,6 +506,8 @@ export class UserTasksComponent implements OnInit {
onTaskTypeChange(): void { onTaskTypeChange(): void {
if (this.SelectedTask['task_type'] === 'snippet') { if (this.SelectedTask['task_type'] === 'snippet') {
this.loadSnippets(); this.loadSnippets();
} else if (this.SelectedTask['task_type'] === 'sequence') {
this.loadSequences();
} }
} }
@ -533,4 +519,22 @@ export class UserTasksComponent implements OnInit {
}); });
}); });
} }
loadSequences(): void {
var _self = this;
_self.data_provider.get_sequences().then((res: any) => {
_self.Sequences = res.map((x: any) => {
return { id: x.id, name: x.name };
});
});
}
onSequenceSelected($event: any) {
if (!this.SelectedTask['data']) this.SelectedTask['data'] = {};
this.SelectedTask['data']['sequence_id'] = $event;
}
onSequencesSearchChanged(v: any) {
// Sequences are fully loaded on open, client side filtering can be used or ignored for now
}
} }

View file

@ -15,7 +15,9 @@ import {
} from "@coreui/angular"; } from "@coreui/angular";
import { UserTasksRoutingModule } from "./user_tasks-routing.module"; import { UserTasksRoutingModule } from "./user_tasks-routing.module";
import { UserTasksComponent } from "./user_tasks.component"; import { UserTasksComponent } from "./user_tasks.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { NgxSuperSelectModule} from "ngx-super-select"; import { NgxSuperSelectModule} from "ngx-super-select";
@ -31,7 +33,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
BadgeModule, BadgeModule,
AlertModule, AlertModule,
ToastModule, ToastModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule, ModalModule,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,

View file

@ -1,22 +1,26 @@
<div style="position: relative;">
<c-card class="mb-4" style="margin-bottom: 0 !important;"> <c-card class="mb-4" style="margin-bottom: 0 !important;">
<c-card-header style="padding: 0;"> <c-card-header style="padding: 0;">
<c-row class="align-items-center"> <c-row class="align-items-center">
<c-col xs="12" md="6"> <c-col xs="12" md="6">
<div class="nav nav-underline bg-transparent"> <div class="nav nav-underline bg-transparent">
<div class="nav-item"> <div class="nav-item">
<a [active]="activetab==0" class="nav-link" [cTabContent]="tabContent" (click)="activetab=0" [tabPaneIdx]="0"> <a [active]="activetab==0" class="nav-link" [cTabContent]="tabContent" (click)="activetab=0"
[tabPaneIdx]="0">
<i class="fas fa-cog me-1"></i>Settings <i class="fas fa-cog me-1"></i>Settings
</a> </a>
</div> </div>
<div class="nav-item"> <div class="nav-item">
<a [active]="activetab==1" class="nav-link" [cTabContent]="tabContent" (click)="get_passwords();activetab=1" [tabPaneIdx]="1"> <a [active]="activetab==1" class="nav-link" [cTabContent]="tabContent" (click)="get_passwords();activetab=1"
[tabPaneIdx]="1">
<i class="fas fa-key me-1"></i>Passwords <i class="fas fa-key me-1"></i>Passwords
</a> </a>
</div> </div>
</div> </div>
</c-col> </c-col>
<c-col xs="12" md="6" class="text-end mt-2 mt-md-0"> <c-col xs="12" md="6" class="text-end mt-2 mt-md-0">
<button *ngIf="activetab==0" cButton size="sm" class="me-2" (click)="runConfirmModalVisible=!runConfirmModalVisible" color="danger"> <button *ngIf="activetab==0" cButton size="sm" class="me-2"
(click)="runConfirmModalVisible=!runConfirmModalVisible" color="danger">
<i class="fas fa-play me-1"></i>Execute Now <i class="fas fa-play me-1"></i>Execute Now
</button> </button>
<button *ngIf="activetab==1" cButton size="sm" (click)="toggleCollapse()" color="info"> <button *ngIf="activetab==1" cButton size="sm" (click)="toggleCollapse()" color="info">
@ -36,7 +40,8 @@
<i class="fas fa-cog text-primary me-2 fs-4"></i> <i class="fas fa-cog text-primary me-2 fs-4"></i>
<div> <div>
<h5 class="mb-1">Password Vault Settings</h5> <h5 class="mb-1">Password Vault Settings</h5>
<p class="text-muted mb-0 small">Configure automated password management for your device groups. Set schedules, define password policies, and manage affected devices.</p> <p class="text-muted mb-0 small">Configure automated password management for your device groups. Set
schedules, define password policies, and manage affected devices.</p>
</div> </div>
</div> </div>
</div> </div>
@ -54,7 +59,8 @@
</select> </select>
</c-col> </c-col>
<c-col xs="12" md="6" lg="3"> <c-col xs="12" md="6" lg="3">
<label class="form-label small">Strategy <i class="fas fa-info-circle text-info ms-1" [cTooltip]="strategyTooltipTemplate" cTooltipPlacement="top"></i></label> <label class="form-label small">Strategy <i class="fas fa-info-circle text-info ms-1"
[cTooltip]="strategyTooltipTemplate" cTooltipPlacement="top"></i></label>
<select cSelect size="sm" [(ngModel)]="settings['strategy']" class="form-select-sm"> <select cSelect size="sm" [(ngModel)]="settings['strategy']" class="form-select-sm">
<option>Choose...</option> <option>Choose...</option>
<option value="all">All Local Users</option> <option value="all">All Local Users</option>
@ -62,7 +68,8 @@
</select> </select>
</c-col> </c-col>
<c-col xs="12" md="6" lg="3"> <c-col xs="12" md="6" lg="3">
<label class="form-label small">Interval <i class="fas fa-info-circle text-info ms-1" [cTooltip]="intervalTooltipTemplate" cTooltipPlacement="top"></i></label> <label class="form-label small">Interval <i class="fas fa-info-circle text-info ms-1"
[cTooltip]="intervalTooltipTemplate" cTooltipPlacement="top"></i></label>
<select cSelect size="sm" [(ngModel)]="settings['interval']" class="form-select-sm"> <select cSelect size="sm" [(ngModel)]="settings['interval']" class="form-select-sm">
<option>Choose...</option> <option>Choose...</option>
<option value="daily">Daily</option> <option value="daily">Daily</option>
@ -74,7 +81,8 @@
</select> </select>
</c-col> </c-col>
<c-col xs="12" md="6" lg="3"> <c-col xs="12" md="6" lg="3">
<label class="form-label small">Password Type <i class="fas fa-info-circle text-info ms-1" [cTooltip]="passwordTypeTooltipTemplate" cTooltipPlacement="top"></i></label> <label class="form-label small">Password Type <i class="fas fa-info-circle text-info ms-1"
[cTooltip]="passwordTypeTooltipTemplate" cTooltipPlacement="top"></i></label>
<select cSelect size="sm" [(ngModel)]="settings['password_type']" class="form-select-sm"> <select cSelect size="sm" [(ngModel)]="settings['password_type']" class="form-select-sm">
<option>Choose...</option> <option>Choose...</option>
<option value="random">Random</option> <option value="random">Random</option>
@ -104,20 +112,26 @@
</c-col> </c-col>
</c-row> </c-row>
<div *ngIf="settings['exceptions']?.length > 0"> <div *ngIf="settings['exceptions']?.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="settings['exceptions']" [columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting"> <p-table [value]="settings['exceptions']" [showGridlines]="true" [stripedRows]="true"
<gui-grid-column header="Username" field="name"> styleClass="p-datatable-sm">
<ng-template let-value="item" let-item="item" let-index="index"> <ng-template pTemplate="header">
<i class="fas fa-user me-2 text-muted"></i>{{value}} <tr>
</ng-template> <th pSortableColumn="name">Username <p-sortIcon field="name"></p-sortIcon></th>
</gui-grid-column> <th style="width: 80px" class="text-center">Actions</th>
<gui-grid-column header="Actions" width="80" field="action" align="center"> </tr>
<ng-template let-value="item.id" let-item="item" let-index="index"> </ng-template>
<button (click)="remove_exception(item)" cButton color="danger" size="sm" variant="outline"> <ng-template pTemplate="body" let-item>
<i class="fas fa-trash"></i> <tr>
</button> <td><i class="fas fa-user me-2 text-muted"></i>{{item}}</td>
</ng-template> <td class="text-center">
</gui-grid-column> <button (click)="remove_exception(item)" cButton color="danger" size="sm" variant="outline"
</gui-grid> pTooltip="Remove Exception">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div> </div>
</div> </div>
<!-- Pre-defined Passwords --> <!-- Pre-defined Passwords -->
@ -137,20 +151,26 @@
</c-col> </c-col>
</c-row> </c-row>
<div *ngIf="settings['passwords']?.length > 0"> <div *ngIf="settings['passwords']?.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="settings['passwords']" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging"> <p-table [value]="settings['passwords']" [showGridlines]="true" [stripedRows]="true"
<gui-grid-column header="Password" field="name"> styleClass="p-datatable-sm">
<ng-template let-value="item" let-item="item" let-index="index"> <ng-template pTemplate="header">
<i class="fas fa-lock me-2 text-muted"></i>•••••••• <tr>
</ng-template> <th pSortableColumn="name">Password <p-sortIcon field="name"></p-sortIcon></th>
</gui-grid-column> <th style="width: 80px" class="text-center">Actions</th>
<gui-grid-column header="Actions" width="80" field="action" align="center"> </tr>
<ng-template let-value="item.id" let-item="item" let-index="index"> </ng-template>
<button cButton color="danger" size="sm" variant="outline" (click)="remove_password(item)"> <ng-template pTemplate="body" let-item>
<i class="fas fa-trash"></i> <tr>
</button> <td><i class="fas fa-lock me-2 text-muted"></i>••••••••</td>
</ng-template> <td class="text-center">
</gui-grid-column> <button cButton color="danger" size="sm" variant="outline" (click)="remove_password(item)"
</gui-grid> pTooltip="Remove Password">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div> </div>
</div> </div>
<!-- Affected Groups --> <!-- Affected Groups -->
@ -167,20 +187,25 @@
</c-col> </c-col>
</c-row> </c-row>
<div *ngIf="Members?.length > 0" class="mb-3"> <div *ngIf="Members?.length > 0" class="mb-3">
<gui-grid [autoResizeWidth]="true" [source]="Members" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging"> <p-table [value]="Members" [showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm">
<gui-grid-column header="Group Name" field="name"> <ng-template pTemplate="header">
<ng-template let-value="item.name" let-item="item" let-index="index"> <tr>
<i class="fas fa-server me-2 text-primary"></i>{{value}} <th pSortableColumn="name">Group Name <p-sortIcon field="name"></p-sortIcon></th>
</ng-template> <th style="width: 80px" class="text-center">Actions</th>
</gui-grid-column> </tr>
<gui-grid-column header="Actions" width="80" field="action" align="center"> </ng-template>
<ng-template let-value="item.id" let-item="item" let-index="index"> <ng-template pTemplate="body" let-item>
<button (click)="delete_group(item.id)" cButton color="danger" size="sm" variant="outline"> <tr>
<i class="fas fa-trash"></i> <td><i class="fas fa-server me-2 text-primary"></i>{{item.name}}</td>
</button> <td class="text-center">
</ng-template> <button (click)="delete_group(item.id)" cButton color="danger" size="sm" variant="outline"
</gui-grid-column> pTooltip="Remove Group">
</gui-grid> <i class="fas fa-trash"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div> </div>
<div class="text-end"> <div class="text-end">
<button cButton color="success" (click)="save_settings()"> <button cButton color="success" (click)="save_settings()">
@ -192,25 +217,28 @@
<!-- Execution History --> <!-- Execution History -->
<div *ngIf="vault_history"> <div *ngIf="vault_history">
<h6 class="mb-3"><i class="fas fa-history me-2"></i>Execution Reports</h6> <h6 class="mb-3"><i class="fas fa-history me-2"></i>Execution Reports</h6>
<gui-grid [autoResizeWidth]="true" [source]="vault_history" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging"> <p-table #dtHistory [value]="vault_history" [paginator]="true" [rows]="10" [showGridlines]="true"
<gui-grid-column header="Start Time" field="started"> [stripedRows]="true" styleClass="p-datatable-sm">
<ng-template let-value="item.started" let-item="item" let-index="index"> <ng-template pTemplate="header">
<small>{{value}}</small> <tr>
</ng-template> <th pSortableColumn="started">Start Time <p-sortIcon field="started"></p-sortIcon></th>
</gui-grid-column> <th pSortableColumn="ended">End Time <p-sortIcon field="ended"></p-sortIcon></th>
<gui-grid-column header="End Time" field="ended"> <th style="width: 100px" class="text-center">Results</th>
<ng-template let-value="item.ended" let-item="item" let-index="index"> </tr>
<small>{{value}}</small> </ng-template>
</ng-template> <ng-template pTemplate="body" let-item>
</gui-grid-column> <tr>
<gui-grid-column header="Results" field="result" align="center" width="100"> <td><small>{{item.started}}</small></td>
<ng-template let-value="item['result']" let-item="item" let-index="index"> <td><small>{{item.ended}}</small></td>
<button (click)="exportToCsv(value)" color="primary" size="sm" cButton variant="outline"> <td class="text-center">
<i class="fas fa-download"></i> <button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton variant="outline"
</button> pTooltip="Download CSV">
</ng-template> <i class="fas fa-download"></i>
</gui-grid-column> </button>
</gui-grid> </td>
</tr>
</ng-template>
</p-table>
</div> </div>
</c-card-body> </c-card-body>
</c-card> </c-card>
@ -224,7 +252,8 @@
<i class="fas fa-key text-warning me-2 fs-4"></i> <i class="fas fa-key text-warning me-2 fs-4"></i>
<div> <div>
<h5 class="mb-1">Stored Passwords</h5> <h5 class="mb-1">Stored Passwords</h5>
<p class="text-muted mb-0 small">View and manage passwords stored by the vault system. Use filters to find specific devices or users.</p> <p class="text-muted mb-0 small">View and manage passwords stored by the vault system. Use filters to find
specific devices or users.</p>
</div> </div>
</div> </div>
</div> </div>
@ -257,41 +286,59 @@
<!-- Stored Passwords --> <!-- Stored Passwords -->
<div *ngIf="passwords"> <div *ngIf="passwords">
<h6 class="mb-3"><i class="fas fa-database me-2"></i>Stored Passwords</h6> <h6 class="mb-3"><i class="fas fa-database me-2"></i>Stored Passwords</h6>
<gui-grid [autoResizeWidth]="true" [source]="passwords" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"> <p-table #dtPasswords [value]="passwords" [paginator]="true" [rows]="10" [showGridlines]="true"
<gui-grid-column header="Device" field="name"> [stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
<ng-template let-value="item.name" let-item="item" let-index="index"> styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
<div class="d-flex align-items-center"> [globalFilterFields]="['name', 'devip', 'username', 'changed']" [loading]="loading">
<i class="fas fa-router me-2 text-primary"></i> <ng-template pTemplate="header">
<div> <tr>
<div class="fw-semibold">{{value}}</div> <th pSortableColumn="name" pResizableColumn>
<small class="text-muted">{{item.devip}}</small> Device
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="username" pResizableColumn>
User
<p-sortIcon field="username"></p-sortIcon>
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="changed" pResizableColumn>
Last Changed
<p-sortIcon field="changed"></p-sortIcon>
<p-columnFilter type="text" field="changed" display="menu" class="ms-auto" />
</th>
<th style="width: 100px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="fas fa-server me-2 text-primary"></i>
<div>
<div class="fw-semibold">{{item.name}}</div>
<small class="text-muted">{{item.devip}}</small>
</div>
</div> </div>
</div> </td>
</ng-template> <td><i class="fas fa-user me-2 text-muted"></i>{{item.username}}</td>
</gui-grid-column> <td><small>{{item.changed}}</small></td>
<gui-grid-column header="Username" field="username"> <td class="text-center">
<ng-template let-value="item.username" let-item="item" let-index="index"> <button cButton *ngIf="ispro" (click)="reveal_password(item.devid,item.username)" color="info"
<i class="fas fa-user me-2 text-muted"></i>{{value}} size="sm" variant="outline" pTooltip="View Password">
</ng-template> <i class="fas fa-eye"></i>
</gui-grid-column> </button>
<gui-grid-column header="Last Changed" field="changed"> </td>
<ng-template let-value="item.changed" let-item="item" let-index="index"> </tr>
<small>{{value}}</small> </ng-template>
</ng-template> </p-table>
</gui-grid-column>
<gui-grid-column header="Actions" width="100" field="action" align="center">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton *ngIf="ispro" (click)="reveal_password(item.devid,item.username)" color="info" size="sm" variant="outline">
<i class="fas fa-eye"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</div> </div>
</c-card-body> </c-card-body>
</c-card> </c-card>
</c-tab-pane> </c-tab-pane>
</c-tab-content> </c-tab-content>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<!-- Password Reveal Modal --> <!-- Password Reveal Modal -->
<c-modal #PasswordModal backdrop="static" [(visible)]="PasswordModalVisible" id="PasswordModal"> <c-modal #PasswordModal backdrop="static" [(visible)]="PasswordModalVisible" id="PasswordModal">
@ -306,7 +353,7 @@
</div> </div>
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="fas fa-key"></i></span> <span class="input-group-text"><i class="fas fa-key"></i></span>
<input [value]="password" cFormControl disabled="true" class="form-control"/> <input [value]="password" cFormControl disabled="true" class="form-control" />
<button cButton color="secondary" variant="outline" (click)="copyToClipboard(password)"> <button cButton color="secondary" variant="outline" (click)="copyToClipboard(password)">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
@ -325,7 +372,8 @@
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>Warning:</strong> This will execute the password vault job and change passwords on all configured device groups. <strong>Warning:</strong> This will execute the password vault job and change passwords on all configured device
groups.
</div> </div>
<p>Are you sure you want to proceed with the password vault execution?</p> <p>Are you sure you want to proceed with the password vault execution?</p>
</c-modal-body> </c-modal-body>
@ -345,25 +393,50 @@
</c-modal-header> </c-modal-header>
<c-modal-body> <c-modal-body>
<p class="text-muted mb-3">Select device groups to include in the password vault task:</p> <p class="text-muted mb-3">Select device groups to include in the password vault task:</p>
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching" <div class="mb-3 d-flex justify-content-end">
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" <span class="p-input-icon-left">
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging"> <i class="pi pi-search"></i>
<gui-grid-column header="Group Name" field="name"> <input type="text" pInputText placeholder="Search groups..." (input)="applyFilterNewMembers($event, 'contains')"
<ng-template let-value="item.name" let-item="item" let-index="index"> class="form-control-sm" />
<i class="fas fa-server me-2 text-primary"></i>{{value}} </span>
</ng-template> </div>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="IP Address" field="ip"> <p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
<ng-template let-value="item.ip" let-item="item" let-index="index"> [stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
{{value}} styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped" [(selection)]="NewMemberRows"
</ng-template> (selectionChange)="onSelectedRowsNewMembers($event)" [globalFilterFields]="['name', 'ip', 'mac']">
</gui-grid-column> <ng-template pTemplate="header">
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="MAC Address" field="mac"> <tr>
<ng-template let-value="item.mac" let-item="item" let-index="index"> <th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
{{value}} <th pSortableColumn="name" pResizableColumn>
</ng-template> Name
</gui-grid-column> <p-sortIcon field="name"></p-sortIcon>
</gui-grid> <p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="ip" pResizableColumn>
IP
<p-sortIcon field="ip"></p-sortIcon>
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="mac" pResizableColumn>
MAC
<p-sortIcon field="mac"></p-sortIcon>
<p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
<td>
<div class="d-flex align-items-center">
<i class="fas fa-server me-2 text-primary"></i>
<strong>{{item.name}}</strong>
</div>
</td>
</tr>
</ng-template>
</p-table>
</c-modal-body> </c-modal-body>
<c-modal-footer> <c-modal-footer>
<button *ngIf="NewMemberRows.length != 0" (click)="add_new_members()" cButton color="primary"> <button *ngIf="NewMemberRows.length != 0" (click)="add_new_members()" cButton color="primary">

View file

@ -1,19 +1,8 @@
import { Component, OnInit, ViewChildren ,QueryList} from "@angular/core"; import { Component, OnInit, ViewChildren, QueryList, ViewChild } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data"; import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker"; import { loginChecker } from "../../providers/login_checker";
import { import { Table } from 'primeng/table';
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { NgxSuperSelectOptions } from "ngx-super-select"; import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform"; import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
@ -27,10 +16,14 @@ import { AppToastComponent } from "../toast-simple/toast.component";
}) })
export class VaultComponent implements OnInit { export class VaultComponent implements OnInit {
public uid: number; public uid: number = 0;
public uname: string; public uname: string = '';
public ispro: boolean = false; public ispro: boolean = false;
public tz: string; public tz: string = '';
@ViewChild('dtPasswords') dtPasswords!: Table;
@ViewChild('dtHistory') dtHistory!: Table;
@ViewChild('dtNewMembers') dtNewMembers!: Table;
constructor( constructor(
private data_provider: dataProvider, private data_provider: dataProvider,
@ -71,7 +64,6 @@ export class VaultComponent implements OnInit {
public password:string=""; public password:string="";
public PasswordModalVisible:boolean=false; public PasswordModalVisible:boolean=false;
public source: Array<any> = []; public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true; public loading: boolean = true;
public rows: any = []; public rows: any = [];
public SelectedTask: any = {}; public SelectedTask: any = {};
@ -87,16 +79,7 @@ export class VaultComponent implements OnInit {
public filters: any = {}; public filters: any = {};
public activetab:number=0; public activetab:number=0;
public sorting = { public toasterForm = {
enabled: true,
multiSorting: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
toasterForm = {
autohide: true, autohide: true,
delay: 3000, delay: 3000,
position: "fixed", position: "fixed",
@ -104,42 +87,17 @@ export class VaultComponent implements OnInit {
closeButton: true, closeButton: true,
}; };
options: Partial<NgxSuperSelectOptions> = { applyFilterPasswords($event: any, stringVal: string) {
selectionMode: "single", this.dtPasswords.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
actionsEnabled: false, }
displayExpr: "name",
valueExpr: "id",
placeholder: "Snippet",
searchEnabled: true,
enableDarkMode: false,
};
public paging: GuiPaging = { applyFilterHistory($event: any, stringVal: string) {
enabled: true, this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
page: 1, }
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = { applyFilterNewMembers($event: any, stringVal: string) {
enabled: true, this.dtNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
sort: true, }
columnsManager: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
reinitgrid(field: string, $event: any) { reinitgrid(field: string, $event: any) {
if (field == "username") this.filters["username"] = $event; if (field == "username") this.filters["username"] = $event;
@ -153,10 +111,9 @@ export class VaultComponent implements OnInit {
this.get_vault_history(); this.get_vault_history();
} }
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void { onSelectedRowsNewMembers(rows: any[]): void {
this.NewMemberRows = rows; this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => ({'id': m.source.id,'name':m.source.name})); this.SelectedNewMemberRows = rows.map((m: any) => ({'id': m.id,'name':m.name}));
} }
toggleCollapse(): void { toggleCollapse(): void {

View file

@ -16,10 +16,13 @@ import {
} from "@coreui/angular"; } from "@coreui/angular";
import { VaultRoutingModule } from "./vault-routing.module"; import { VaultRoutingModule } from "./vault-routing.module";
import { VaultComponent } from "./vault.component"; import { VaultComponent } from "./vault.component";
import { GuiGridModule } from "@generic-ui/ngx-grid"; import { TableModule as PTableModule } from 'primeng/table';
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { MatInputModule } from "@angular/material/input"; import { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field"; import { MatFormFieldModule } from "@angular/material/form-field";
import { SharedModule } from "../../shared/shared.module";
@NgModule({ @NgModule({
imports: [ imports: [
VaultRoutingModule, VaultRoutingModule,
@ -29,7 +32,9 @@ import { MatFormFieldModule } from "@angular/material/form-field";
FormModule, FormModule,
ButtonModule, ButtonModule,
ButtonGroupModule, ButtonGroupModule,
GuiGridModule, PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule, ModalModule,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
@ -38,7 +43,8 @@ import { MatFormFieldModule } from "@angular/material/form-field";
MatInputModule, MatInputModule,
MatFormFieldModule, MatFormFieldModule,
CollapseModule, CollapseModule,
TooltipModule TooltipModule,
SharedModule
], ],
declarations: [VaultComponent], declarations: [VaultComponent],
}) })

View file

@ -63,12 +63,6 @@
} }
} }
// Grid Improvements
gui-grid {
.gui-grid {
font-size: 0.85rem;
}
}
// Input Group Compact // Input Group Compact
.input-group-sm { .input-group-sm {
@ -204,19 +198,6 @@ gui-grid {
} }
} }
// Grid Cell Improvements
::ng-deep gui-grid {
.gui-grid-cell {
padding: 0.5rem;
font-size: 0.85rem;
}
.gui-grid-header-cell {
font-size: 0.8rem;
font-weight: 600;
padding: 0.5rem;
}
}
// Tab Info Headers // Tab Info Headers
.tab-info-header { .tab-info-header {

View file

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { VpnComponent } from './vpn.component';
const routes: Routes = [
{
path: '',
component: VpnComponent,
data: {
title: 'VPN Server'
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class VpnRoutingModule {
}

View file

@ -0,0 +1,656 @@
<div style="position: relative;">
<div class="fade-in">
<!-- Communication Error Alert -->
<c-row class="mb-4" *ngIf="isCommunicationError">
<c-col sm="12">
<div class="alert alert-danger shadow-sm border-0 d-flex align-items-start p-4 mb-0">
<div class="me-3 fs-2">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div>
<h5 class="alert-heading fw-bold">Need to check docker container and wireguard server</h5>
<p class="mb-2">For this feature, the MikroWizard WireGuard server must be installed. If already installed,
please verify that the Docker container is running and accessible.</p>
<hr>
<div class="d-flex align-items-center">
<span class="me-2 text-muted fw-semibold">Troubleshooting and installation Instruction:</span>
<a href="https://mikrowizard.com/docs/install-mikrowizard-wireguard-docker-server/" target="_blank"
class="btn btn-danger btn-sm text-decoration-none">
<i class="fas fa-external-link-alt me-1"></i> Install Mikrowizard wireguard docker server
</a>
</div>
</div>
</div>
</c-col>
</c-row>
<!-- Top Cards -->
<c-row class="mb-4 d-flex justify-content-between">
<!-- Server Status Card -->
<c-col sm="4">
<c-card class="shadow-sm border-0 h-100"
[ngClass]="{'bg-success text-white': status?.status === 'running' && !isCommunicationError, 'bg-danger text-white': status?.status === 'error' || isCommunicationError || !status, 'bg-warning text-dark': status?.status === 'setup_required' && !isCommunicationError}">
<c-card-body class="p-3 d-flex align-items-center">
<div class="me-3 fs-2 opacity-75">
<i class="fas fa-server"></i>
</div>
<div>
<div class="text-uppercase fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px; opacity: 0.8;">
Server Status</div>
<div class="fs-5 fw-bold mt-1">
{{isCommunicationError ? 'Communication Failed' : (status?.status === 'running' ? 'Running' :
(status?.status === 'setup_required' ? 'Setup Required' :
'Error/Offline'))}}
</div>
</div>
</c-card-body>
</c-card>
</c-col>
<!-- Total Peers Card -->
<c-col sm="4">
<c-card class="shadow-sm border-0 h-100 bg-primary text-white">
<c-card-body class="p-3 d-flex align-items-center">
<div class="me-3 fs-2 opacity-75">
<i class="fas fa-network-wired"></i>
</div>
<div>
<div class="text-uppercase fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px; opacity: 0.8;">
Connected Peers</div>
<div class="fs-5 fw-bold mt-1">
{{ source.length }} Active
</div>
</div>
</c-card-body>
</c-card>
</c-col>
<!-- Total Traffic Card -->
<c-col sm="4">
<c-card class="shadow-sm border-0 h-100 bg-info text-white position-relative">
<button class="btn btn-sm btn-light position-absolute top-0 end-0 m-2" style="opacity: 0.8; z-index: 2;"
(click)="promptResetServer()" cTooltip="Reset Server Counters">
<i class="fa-solid fa-redo"></i>
</button>
<c-card-body class="p-3 d-flex align-items-center">
<div class="me-3 fs-3 opacity-75">
<i class="fas fa-satellite-dish"></i>
</div>
<div class="w-100">
<div class="d-flex justify-content-between text-uppercase fw-semibold"
style="font-size: 0.70rem; letter-spacing: 0.5px; opacity: 0.8;">
<span>Live Speed</span>
<span>Total Vol</span>
</div>
<div class="d-flex justify-content-between mt-1 w-100">
<!-- Live Speeds -->
<div class="d-flex flex-column" style="font-size: 0.8rem; font-weight: 500;">
<span class="text-nowrap" [ngClass]="{'text-success': liveSpeedRx !== 0}">⬇ {{ formatBytes(liveSpeedRx)
}}/s</span>
<span class="text-nowrap" [ngClass]="{'text-warning': liveSpeedTx !== 0}">⬆ {{ formatBytes(liveSpeedTx)
}}/s</span>
</div>
<!-- Total Volumes -->
<div class="d-flex flex-column text-end" style="font-size: 0.8rem; font-weight: 500;">
<span class="text-nowrap">⬇ {{ (totalRx / 1048576) | number:'1.1-2' }} MB</span>
<span class="text-nowrap">⬆ {{ (totalTx / 1048576) | number:'1.1-2' }} MB</span>
</div>
</div>
</div>
</c-card-body>
</c-card>
</c-col>
</c-row>
<!-- Traffic Chart -->
<c-row class="mb-4">
<c-col sm="12">
<c-card class="shadow-sm border-0">
<c-card-header>
<strong>Live Server Traffic</strong> <small class="text-muted">(Total Cumulative MB)</small>
</c-card-header>
<c-card-body>
<div class="chart-wrapper" style="height: 250px;">
<c-chart [data]="chartData" [options]="chartOptions" type="line" height="250"></c-chart>
</div>
</c-card-body>
</c-card>
</c-col>
</c-row>
<!-- Peers Table -->
<c-row>
<c-col xs>
<c-card class="mb-4">
<c-card-header>
<c-row>
<c-col xs [lg]="3">
<strong>VPN Peers</strong>
</c-col>
<c-col xs [lg]="9">
<h6 style="text-align: right;">
<button cButton color="primary" (click)="openServerConfigModal()" class="mx-1" size="sm">
<i class="fas fa-cogs"></i> Server Config
</button>
<button cButton color="success" (click)="openAddPeerModal()" class="mx-1" size="sm"
style="color: #fff;">
<i class="fa-solid fa-plus"></i> Add Peer
</button>
</h6>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<div class="mb-3 d-flex justify-content-end">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search peers..."
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
[showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['name', 'assigned_ip', 'public_key', 'description']" [loading]="loading">
<ng-template pTemplate="header">
<tr>
<th style="width: 50px" class="text-center" pResizableColumn>Status</th>
<th pSortableColumn="name" style="width: 200px" pResizableColumn>
<div class="d-flex justify-content-between">
<span>Identity & IP</span>
<span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="description" pResizableColumn>
<div class="d-flex justify-content-between">
<span>Details & Description</span>
<span>
<p-sortIcon field="description"></p-sortIcon>
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="nat_mode" style="width: 140px" pResizableColumn>
<div class="d-flex justify-content-between">
<span>Routing</span>
<span>
<p-sortIcon field="nat_mode"></p-sortIcon>
<p-columnFilter type="text" field="nat_mode" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th style="width: 220px" class="text-center" pResizableColumn>Integration</th>
<th pSortableColumn="stats.last_handshake" style="width: 130px" class="text-center" pResizableColumn>
<div class="d-flex justify-content-between">
<span>Last Handshake</span>
<p-sortIcon field="stats.last_handshake"></p-sortIcon>
</div>
</th>
<th style="width: 130px" pResizableColumn>Transfer Speed</th>
<th style="width: 120px" pResizableColumn>Total Volume</th>
<th pSortableColumn="is_enabled" style="width: 90px" class="text-center" pResizableColumn>
<div class="d-flex justify-content-between">
<span>State</span>
<p-sortIcon field="is_enabled"></p-sortIcon>
</div>
</th>
<th style="width: 70px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr [ngClass]="{'row-highlighted': item.status === 'online'}">
<td class="text-center">
<i *ngIf="item.status === 'online'" class="fa-solid fa-circle text-success" [pTooltip]="'Online'"
tooltipPosition="top"></i>
<i *ngIf="item.status === 'offline'" class="fa-solid fa-circle text-danger" [pTooltip]="'Offline'"
tooltipPosition="top"></i>
<i *ngIf="item.status === 'unreachable'" class="fa-solid fa-triangle-exclamation text-warning"
[pTooltip]="'Unreachable (Handshake OK, Ping Failed)'" tooltipPosition="top"></i>
<i *ngIf="!item.status && item.is_enabled" class="fa-solid fa-circle text-info" [pTooltip]="'Enabled'"
tooltipPosition="top"></i>
<i *ngIf="!item.status && !item.is_enabled" class="fa-solid fa-circle text-secondary"
[pTooltip]="'Disabled'" tooltipPosition="top"></i>
</td>
<td>
<div style="line-height: 1.2;">
<i *ngIf="item.mt_user" class="fas fa-server text-info me-1" [pTooltip]="'Managed MikroTik Device'"
style="vertical-align: middle; font-size: 0.9rem;"></i>
<strong class="text-primary fs-6 text-truncate d-inline-block"
style="max-width: 140px; vertical-align: middle;" [title]="item.name || item.assigned_ip">
{{ item.name || item.assigned_ip || (item.public_key | slice:0:8) + '...' }}
</strong>
<c-badge *ngIf="item.is_managed === false" color="secondary" class="ms-1"
style="vertical-align: middle; font-size: 0.65rem;">Unmanaged</c-badge>
</div>
<div style="font-size: 0.85rem; color: #495057;" class="fw-semibold ms-2">
<i class="fas fa-network-wired opacity-75"></i> {{ item.assigned_ip || 'No IP' }}
</div>
</td>
<td>
<div class="d-flex flex-column justify-content-center w-100">
<div *ngIf="item.description" class="text-muted mb-1 text-wrap small fw-bold"
[title]="item.description">
{{ item.description }}
</div>
<div style="color: #adb5bd; font-size: 0.75rem" class="d-flex flex-row align-items-center gap-3">
<div class="text-truncate" title="Public Key" style="max-width: 140px;">
<i class="fas fa-key me-1 opacity-75"></i> {{ item.public_key | slice:0:16 }}...
</div>
<div class="text-truncate" title="Created At">
<i class="fas fa-clock me-1 opacity-75"></i> {{ item.created_at | date:'mediumDate' }}
</div>
</div>
</div>
</td>
<td>
<div class="mb-1">
<c-badge size="sm"
[color]="item.nat_mode === 'full' ? 'success' : (item.nat_mode === 'split' ? 'info' : 'secondary')">
{{ item.nat_mode | uppercase }}
</c-badge>
</div>
<div style="font-size: 0.75rem;" class="text-muted mt-1">
<i class="fas fa-network-wired small opacity-50"></i> Iface: {{ item.custom_interface || 'Auto' }}
</div>
</td>
<td class="text-center">
<div class="d-flex flex-column align-items-center gap-1">
<a *ngIf="item.linked_device_id" [routerLink]="['/device-stats', {id: item.linked_device_id}]"
target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.7rem;">
<i class="fa-solid fa-link"></i> View Device
</a>
<div class="d-flex flex-row justify-content-center w-100 container-fluid p-0">
<small *ngIf="!item.linked_device_id && !item.scan_status && item.mt_user" class="text-muted"
style="font-size: 0.65rem;">Not Linked</small>
<div style="font-size: 0.7rem;"
*ngIf="item.scan_status === 'starting' || item.scan_status === 'running'" class="text-info">
<i class="fas fa-circle-notch fa-spin me-1"></i> Scanning...
</div>
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'completed'" class="text-success">
<i class="fas fa-check-circle me-1"></i> Success
</div>
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'failed'" class="text-danger"
[pTooltip]="'MikroTik connection failed'" tooltipPosition="top">
<i class="fas fa-exclamation-triangle me-1"></i> Failed
</div>
</div>
<div style="font-size: 0.65rem;" class="text-secondary opacity-75">
<i class="fas fa-heartbeat"></i> Keepalive: {{ item.persistent_keepalive }}s
</div>
</div>
</td>
<td class="text-center small">
<div *ngIf="item.stats?.last_handshake > 0" class="text-secondary fw-semibold">
<i class="fas fa-handshake"></i> {{ item.stats.last_handshake * 1000 | date:'shortTime' }}<br>
<small class="text-muted">{{ item.stats.last_handshake * 1000 | date:'shortDate' }}</small>
</div>
<div *ngIf="!item.stats?.last_handshake || item.stats.last_handshake === 0" class="text-muted">
<i class="fas fa-handshake"></i> None
</div>
</td>
<td>
<div *ngIf="item.stats" class="d-flex flex-column"
style="font-size: 0.72rem; font-family: monospace; font-weight: bold;">
<span [style.color]="item.stats.rx_speed !== 0 ? '#2eb85c' : '#8a93a2'">
<i class="fas fa-arrow-down opacity-75"></i> {{ formatBytes(item.stats.rx_speed || 0) }}/s
</span>
<span [style.color]="item.stats.tx_speed !== 0 ? '#3399ff' : '#8a93a2'">
<i class="fas fa-arrow-up opacity-75"></i> {{ formatBytes(item.stats.tx_speed || 0) }}/s
</span>
</div>
</td>
<td>
<div *ngIf="item.stats" class="d-flex flex-column" style="font-size: 0.7rem;">
<span class="text-success"><i class="fas fa-arrow-down opacity-50"></i> {{(item.stats.rx_bytes /
1048576) | number:'1.2-2'}} MB</span>
<span class="text-info"><i class="fas fa-arrow-up opacity-50"></i> {{(item.stats.tx_bytes / 1048576)
| number:'1.2-2'}} MB</span>
</div>
</td>
<td class="text-center">
<c-form-check [switch]="true" class="d-inline-block">
<input cFormCheckInput type="checkbox" [checked]="item.is_enabled"
(click)="$event.preventDefault(); promptToggleEnabled(item)"
[disabled]="item.is_managed === false" [id]="'switch-peer-' + item.public_key" />
</c-form-check>
</td>
<td class="text-center">
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
[matMenuTriggerFor]="menu" cButton>
<i class="fa-solid fa-bars"></i>
</button>
<mat-menu #menu="matMenu">
<div cListGroup>
<button size="sm" cListGroupItem (click)="openEditModal(item)" *ngIf="item.is_managed !== false">
<i class="fa-solid fa-pencil text-primary"></i><small> Edit</small>
</button>
<button *ngIf="item.mt_user && !item.linked_device_id" size="sm" cListGroupItem
(click)="scanDevice(item)">
<i class="fa-solid fa-magnifying-glass text-info"></i><small> Manual Scan</small>
</button>
<button size="sm" cListGroupItem (click)="openScriptModal(item)">
<i class="fa-solid fa-terminal text-secondary"></i><small> MikroTik Script</small>
</button>
<button size="sm" cListGroupItem (click)="openQrModal(item)">
<i class="fa-solid fa-qrcode text-secondary"></i><small> QR Code</small>
</button>
<button size="sm" cListGroupItem (click)="downloadPeerConfigDirect(item)">
<i class="fa-solid fa-download text-secondary"></i><small> Download Config</small>
</button>
<button size="sm" cListGroupItem (click)="promptResetPeer(item)">
<i class="fa-solid fa-sync text-warning"></i><small> Reset Counters</small>
</button>
<button size="sm" cListGroupItem (click)="confirmDelete(item)">
<i class="fa-solid fa-trash text-danger"></i><small> Delete</small>
</button>
</div>
</mat-menu>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="10" class="text-center p-4">No VPN peers found.</td>
</tr>
</ng-template>
</p-table>
</c-card-body>
</c-card>
</c-col>
</c-row>
<!-- MODALS -->
<!-- Server Config Modal -->
<c-modal #ServerConfigModal backdrop="static" [(visible)]="serverConfigModalVisible" id="ServerConfigModal">
<c-modal-header>
<h6 cModalTitle>VPN Server Configuration</h6>
<button [cModalToggle]="ServerConfigModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<span cInputGroupText>API Endpoint</span>
<input cFormControl [(ngModel)]="serverConfig.api_endpoint" placeholder="http://127.0.0.1:5000" />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText>API Token</span>
<input cFormControl type="password" [(ngModel)]="serverConfig.api_token" placeholder="Optional Auth Token" />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText>VPN Subnet</span>
<input cFormControl [(ngModel)]="serverConfig.vpn_subnet" placeholder="10.8.0.0/24" />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText>Public Server IP/Host</span>
<input cFormControl [(ngModel)]="serverConfig.public_server_ip" placeholder="vpn.mikrowizard.com" />
</c-input-group>
</c-modal-body>
<c-modal-footer>
<button cButton color="primary" (click)="saveServerConfig()">Save Changes</button>
<button cButton color="secondary" (click)="serverConfigModalVisible = false">Cancel</button>
</c-modal-footer>
</c-modal>
<!-- Delete Confirm Modal -->
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="deleteModalVisible" id="DeleteConfirmModal">
<c-modal-header>
<h6 cModalTitle class="text-danger">Warning: Delete Peer</h6>
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<p class="text-danger">
<em>Warning: This will permanently remove the VPN tunnel and NAT rules for this peer. Connected devices will
lose connection immediately.</em>
</p>
<p>Are you sure you want to delete peer <strong>{{ peerToDelete?.assigned_ip }}</strong>?</p>
</c-modal-body>
<c-modal-footer>
<button cButton color="danger" (click)="executeDelete()">Yes, Delete Peer</button>
<button cButton color="secondary" (click)="deleteModalVisible = false">Cancel</button>
</c-modal-footer>
</c-modal>
<!-- Toggle Confirm Modal -->
<c-modal #ToggleConfirmModal backdrop="static" [(visible)]="toggleModalVisible" id="ToggleConfirmModal">
<c-modal-header>
<h6 cModalTitle>Confirm Status Change</h6>
<button [cModalToggle]="ToggleConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<p>Are you sure you want to <strong [ngClass]="peerToToggle?.is_enabled ? 'text-danger' : 'text-success'">{{
peerToToggle?.is_enabled ? 'Disable' : 'Enable' }}</strong> peer <strong class="text-primary">{{
peerToToggle?.assigned_ip }}</strong>?</p>
</c-modal-body>
<c-modal-footer>
<button cButton [color]="peerToToggle?.is_enabled ? 'danger' : 'success'" (click)="confirmToggle()">Yes, {{
peerToToggle?.is_enabled ? 'Disable' : 'Enable' }}</button>
<button cButton color="secondary" (click)="toggleModalVisible = false">Cancel</button>
</c-modal-footer>
</c-modal>
<!-- Script Viewer Modal -->
<c-modal #ScriptModal backdrop="static" [fullscreen]="true" [(visible)]="scriptModalVisible" id="ScriptModal">
<c-modal-header>
<h6 cModalTitle>MikroTik Provisioning Script</h6>
<button [cModalToggle]="ScriptModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div *ngIf="configResult.script">
<div style="overflow-y: auto; background-color: #f8f9fa;">
<div highlight-js lang="routeros" [options]="{}">{{
configResult.script }}</div>
</div>
<div class="mt-3 text-end">
<button cButton color="primary" [cdkCopyToClipboard]="configResult.script || ''" (click)="copyScript()">
<i class="fas fa-copy"></i> Copy to Clipboard
</button>
</div>
</div>
</c-modal-body>
<c-modal-footer>
<button cButton color="secondary" (click)="scriptModalVisible = false">Close</button>
</c-modal-footer>
</c-modal>
<!-- QR Viewer Modal -->
<c-modal #QrModal backdrop="static" [(visible)]="qrModalVisible" id="QrModal">
<c-modal-header>
<h6 cModalTitle>Mobile Quick Setup (QR Code)</h6>
<button [cModalToggle]="QrModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div *ngIf="configResult.qrBlobUrl" class="text-center mb-3">
<img [src]="configResult.qrBlobUrl" alt="WireGuard QR Code"
style="max-width: 250px; border: 1px solid #ddd; padding: 10px; border-radius: 8px;" />
</div>
<div class="alert alert-info py-2 m-0" style="font-size: 0.85rem;">
<i class="fas fa-info-circle"></i> <strong>How to connect:</strong> Open the official WireGuard app on your
mobile device, tap the <i class="fas fa-plus"></i> button, and select <strong>"Create from QR code"</strong>.
</div>
</c-modal-body>
<c-modal-footer class="d-flex justify-content-between w-100">
<div>
<button cButton color="primary" variant="outline" size="sm" class="me-2"
(click)="activePeerConfig && downloadPeerConfigDirect(activePeerConfig)">
<i class="fas fa-download"></i> Download Config
</button>
<button cButton color="info" variant="outline" size="sm"
(click)="activePeerConfig && openScriptModal(activePeerConfig)">
<i class="fas fa-terminal"></i> Show Script
</button>
</div>
<button cButton color="secondary" (click)="qrModalVisible = false">Close</button>
</c-modal-footer>
</c-modal>
<!-- Add / Edit Peer Wizard Modal -->
<c-modal #AddPeerModal backdrop="static" size="lg" [(visible)]="addPeerModalVisible" id="AddPeerModal">
<c-modal-header>
<h5 cModalTitle>{{ editingPeer ? 'Edit VPN Peer' : 'Create New VPN Peer' }}</h5>
<button [cModalToggle]="AddPeerModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<!-- Step 1: General Info -->
<div *ngIf="addPeerStep === 1" style="animation: fadeIn 0.3s ease-in-out;">
<h6 class="mb-4 text-primary">Step 1: General Info</h6>
<c-input-group class="mb-3">
<span cInputGroupText><i class="fas fa-tag"></i></span>
<input cFormControl [(ngModel)]="peerForm.name" placeholder="Peer Name (e.g. CEO Laptop)" />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText><i class="fas fa-align-left"></i></span>
<textarea cFormControl [(ngModel)]="peerForm.description" placeholder="Optional description..."
rows="2"></textarea>
</c-input-group>
</div>
<!-- Step 2: Addressing -->
<div *ngIf="addPeerStep === 2" style="animation: fadeIn 0.3s ease-in-out;">
<h6 class="mb-4 text-primary">Step 2: Addressing</h6>
<c-input-group class="mb-3">
<span cInputGroupText>Assigned IP</span>
<input cFormControl [(ngModel)]="peerForm.custom_ip" placeholder="Leave empty for Auto-assign" />
</c-input-group>
<c-input-group class="mb-3" *ngIf="editingPeer">
<span cInputGroupText>Public Key</span>
<input cFormControl [(ngModel)]="peerForm.pubkey" readonly />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText>Keepalive</span>
<input cFormControl type="number" [(ngModel)]="peerForm.persistent_keepalive" placeholder="25" />
<span cInputGroupText>seconds</span>
</c-input-group>
</div>
<!-- Step 3: Routing -->
<div *ngIf="addPeerStep === 3" style="animation: fadeIn 0.3s ease-in-out;">
<h6 class="mb-4 text-primary">Step 3: Routing</h6>
<c-input-group class="mb-3">
<span cInputGroupText>NAT Mode</span>
<select cSelect [(ngModel)]="peerForm.nat_mode">
<option value="full">Full Tunnel (Route all traffic)</option>
<option value="split">Split Tunnel (Route specific subnets)</option>
<option value="off">Off (Direct Routing)</option>
</select>
</c-input-group>
<div *ngIf="peerForm.nat_mode === 'split'" class="mt-3">
<h6>Split Tunnel Targets</h6>
<div *ngFor="let target of peerForm.split_targets; let i = index; trackBy: trackByIndex" class="d-flex mb-2">
<input cFormControl [(ngModel)]="peerForm.split_targets[i]" placeholder="e.g. 192.168.1.0/24"
class="me-2" />
<button cButton color="danger" variant="outline" (click)="removeSplitTarget(i)"><i
class="fas fa-trash"></i></button>
</div>
<button cButton color="info" size="sm" variant="outline" (click)="addSplitTarget()">
<i class="fas fa-plus"></i> Add Subnet
</button>
</div>
</div>
<!-- Step 4: Identity -->
<div *ngIf="addPeerStep === 4" style="animation: fadeIn 0.3s ease-in-out;">
<h6 class="mb-4 text-primary">Step 4: Identity & Integrations</h6>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="linkDeviceCheck" [(ngModel)]="peerForm.link_device">
<label class="form-check-label" for="linkDeviceCheck">
Link & Manage as MikroTik Device
</label>
</div>
<div *ngIf="peerForm.link_device" class="mt-3" style="animation: fadeIn 0.3s ease-in-out;">
<c-input-group class="mb-3">
<span cInputGroupText><i class="fas fa-user"></i></span>
<input cFormControl [(ngModel)]="peerForm.mt_user" placeholder="MikroTik Username" />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText><i class="fas fa-lock"></i></span>
<input cFormControl type="password" [(ngModel)]="peerForm.mt_pass" placeholder="MikroTik Password" />
</c-input-group>
<c-input-group class="mb-3">
<span cInputGroupText><i class="fas fa-network-wired"></i></span>
<input cFormControl type="number" [(ngModel)]="peerForm.mt_port" placeholder="8728" />
</c-input-group>
<div class="alert alert-info mt-3" style="font-size: 0.85rem;">
<i class="fas fa-info-circle"></i> Once the peer connects and routes traffic (Online), the MikroWizard
native scanner will automatically use these credentials in the background to discover and safely add the
router into your Devices panel!
</div>
</div>
</div>
<!-- Step 5: Summary -->
<div *ngIf="addPeerStep === 5" style="animation: fadeIn 0.3s ease-in-out;">
<h6 class="mb-4 text-primary">Step 5: Summary</h6>
<ul class="list-group">
<li class="list-group-item"><strong>IP Assignment Preview:</strong> {{ peerForm.custom_ip || 'Auto-assigned'
}}</li>
<li class="list-group-item"><strong>Routing NAT Mode:</strong> <c-badge color="info">{{ peerForm.nat_mode |
uppercase }}</c-badge></li>
<li class="list-group-item" *ngIf="peerForm.nat_mode === 'split'"><strong>Split Targets:</strong> {{
peerForm.split_targets.join(', ') || 'None' }}</li>
<li class="list-group-item"><strong>MikroTik Integration:</strong> {{ peerForm.link_device ? 'Enabled' :
'Disabled' }}</li>
</ul>
</div>
</c-modal-body>
<c-modal-footer>
<button *ngIf="addPeerStep > 1" cButton color="secondary" (click)="addPeerStep = addPeerStep - 1">Back</button>
<button *ngIf="addPeerStep < 5" cButton color="primary" (click)="addPeerStep = addPeerStep + 1">Next</button>
<button *ngIf="addPeerStep === 5" cButton color="success" (click)="submitPeer()">
<i class="fas fa-save"></i> {{ editingPeer ? 'Save Changes' : 'Create Peer' }}
</button>
</c-modal-footer>
</c-modal>
<!-- Server Reset Confirm Modal -->
<c-modal id="resetServerModal" [visible]="resetServerModalVisible" (visibleChange)="resetServerModalVisible = $event">
<c-modal-header class="bg-warning text-dark">
<h5 cModalTitle><i class="fas fa-exclamation-triangle"></i> Reset Server Counters</h5>
<button cButtonClose (click)="resetServerModalVisible = false"></button>
</c-modal-header>
<c-modal-body>
Are you sure you want to reset all data transfer counters for the VPN Server?
<br><br>
<small class="text-muted"><i class="fas fa-info-circle"></i> This will immediately set all Total Rx/Tx metrics to
zero. This does not disconnect any peers.</small>
</c-modal-body>
<c-modal-footer>
<button cButton color="secondary" (click)="resetServerModalVisible = false">Cancel</button>
<button cButton color="warning" (click)="confirmResetServer()">Reset Counters</button>
</c-modal-footer>
</c-modal>
<!-- Peer Reset Confirm Modal -->
<c-modal id="resetPeerModal" [visible]="resetPeerModalVisible" (visibleChange)="resetPeerModalVisible = $event">
<c-modal-header class="bg-warning text-dark">
<h5 cModalTitle><i class="fas fa-exclamation-triangle"></i> Reset Peer Counters</h5>
<button cButtonClose (click)="resetPeerModalVisible = false"></button>
</c-modal-header>
<c-modal-body *ngIf="peerToReset">
Are you sure you want to reset the traffic counters for peer: <strong>{{ peerToReset.name ||
peerToReset.assigned_ip }}</strong>?
<br><br>
<small class="text-muted"><i class="fas fa-info-circle"></i> This clears their individual Rx/Tx metrics to zero
and does not disconnect them.</small>
</c-modal-body>
<c-modal-footer>
<button cButton color="secondary" (click)="resetPeerModalVisible = false">Cancel</button>
<button cButton color="warning" (click)="confirmResetPeer()">Reset Counters</button>
</c-modal-footer>
</c-modal>
</div>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -0,0 +1,53 @@
/* Scss styling for vpn component */
::ng-deep pre {
display: block !important;
min-height: 60vh;
}
.shadow-sm {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
}
.border-0 {
border: 0 !important;
}
.bg-success {
background: linear-gradient(135deg, #2eb85c 0%, #1d823e 100%) !important;
}
.bg-danger {
background: linear-gradient(135deg, #e55353 0%, #ba2828 100%) !important;
}
.bg-primary {
background: linear-gradient(135deg, #321fdb 0%, #1f1498 100%) !important;
}
.bg-info {
background: linear-gradient(135deg, #39f 0%, #0076e6 100%) !important;
}
.opacity-75 {
opacity: 0.75;
}
.text-white {
color: #fff !important;
}
.h-100 {
height: 100% !important;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View file

@ -0,0 +1,589 @@
import { Component, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { loginChecker } from '../../providers/login_checker';
import { VpnService, VpnStatusResponse, VpnPeer, VpnServerConfig } from '../../providers/mikrowizard/vpn.service';
import { Subscription, delay, of, repeat, switchMap, timer, catchError } from 'rxjs';
import { Table } from 'primeng/table';
import { ToasterComponent, ToasterPlacement } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component";
@Component({
templateUrl: 'vpn.component.html',
styleUrls: ['vpn.component.scss']
})
export class VpnComponent implements OnInit, OnDestroy {
@ViewChild("dt") table!: Table;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
toasterForm = {
autohide: true,
delay: 3000,
position: 'fixed' as ToasterPlacement,
fade: true,
closeButton: true,
};
public status: VpnStatusResponse | null = null;
private pollingSubscription?: Subscription;
private livePollingSubscription?: Subscription;
// Aggregate stats
public totalRx: number = 0;
public totalTx: number = 0;
public liveSpeedRx: number = 0;
public liveSpeedTx: number = 0;
public isCommunicationError: boolean = false;
formatBytes(bytes: number, decimals: number = 2): string {
if (!+bytes) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
public chartData: any = {
labels: [],
datasets: [
{
label: 'Rx Speed',
backgroundColor: 'rgba(46, 184, 92, 0.1)',
borderColor: '#2eb85c',
pointBackgroundColor: '#2eb85c',
pointHoverBackgroundColor: '#fff',
borderWidth: 2,
fill: true,
data: []
},
{
label: 'Tx Speed',
backgroundColor: 'rgba(51, 153, 255, 0.1)',
borderColor: '#3399ff',
pointBackgroundColor: '#3399ff',
pointHoverBackgroundColor: '#fff',
borderWidth: 2,
fill: true,
data: []
}
]
};
public chartOptions: any = {
maintainAspectRatio: false,
plugins: {
legend: { display: true },
tooltip: {
callbacks: {
label: (context: any) => {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += this.formatBytes(context.parsed.y) + '/s';
}
return label;
}
}
}
},
scales: {
x: { display: true },
y: {
display: true,
beginAtZero: true,
ticks: {
callback: (value: any) => {
return this.formatBytes(value) + '/s';
}
}
}
},
elements: {
line: { tension: 0.4 },
point: { radius: 0, hitRadius: 10, hoverRadius: 4 }
}
};
// Grid configs
public source: Array<VpnPeer> = [];
public loading: boolean = true;
// Modals state
public addPeerModalVisible = false;
public addPeerStep = 1;
public peerForm: any = {
pubkey: '',
custom_ip: '',
name: '',
description: '',
nat_mode: 'full',
split_targets: [''], // Array of strings
link_device: false,
persistent_keepalive: 25,
custom_interface: '',
mt_user: '',
mt_pass: '',
mt_port: 8728
};
public editingPeer = false;
public configResult: { script?: string, qrBlobUrl?: SafeUrl } = {};
public activePeerConfig: VpnPeer | null = null;
public serverConfigModalVisible = false;
public serverConfig: Partial<VpnServerConfig> = {};
public deleteModalVisible = false;
public peerToDelete: VpnPeer | null = null;
public toggleModalVisible = false;
public peerToToggle: VpnPeer | null = null;
public resetPeerModalVisible = false;
public peerToReset: VpnPeer | null = null;
public resetServerModalVisible = false;
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
constructor(
private login_checker: loginChecker,
private router: Router,
private vpnService: VpnService,
private sanitizer: DomSanitizer
) {
if (!this.login_checker.isLoggedIn()) {
setTimeout(() => this.router.navigate(["login"]), 100);
}
}
ngOnInit(): void {
this.startPolling();
}
ngOnDestroy(): void {
if (this.pollingSubscription) {
this.pollingSubscription.unsubscribe();
}
if (this.livePollingSubscription) {
this.livePollingSubscription.unsubscribe();
}
}
startPolling() {
// Slow poll for heavy metadata (10s)
this.pollingSubscription = timer(0, 10000).pipe(
switchMap(() => this.vpnService.getStatus().pipe(
catchError(err => {
console.error("VPN Polling Error:", err);
return of(null);
})
))
).subscribe({
next: (res) => {
if (res) {
this.status = res;
this.isCommunicationError = res.status === 'failed' && (res.error?.includes('Connection refused') || false);
if (!this.isCommunicationError) {
this.source = (res.peers || []).map(p => ({
...p,
_search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}`
}));
this.computeTotals();
this.loading = false;
} else {
this.loading = false;
this.source = [];
}
} else {
if (!this.status) {
this.loading = true; // Show loading if we never got a successful status
}
}
}
});
// Fast poll for live bandwidth (2s)
this.livePollingSubscription = timer(2000, 2000).pipe(
switchMap(() => this.vpnService.getLiveStatus().pipe(
catchError(err => of(null))
))
).subscribe({
next: (liveData) => {
if (liveData) {
// Update server top card speeds
this.liveSpeedRx = liveData.server.rx_speed || 0;
this.liveSpeedTx = liveData.server.tx_speed || 0;
this.totalRx = liveData.server.rx_bytes;
this.totalTx = liveData.server.tx_bytes;
// Surgically update existing peers without re-creating array
if (this.source && this.source.length > 0 && Array.isArray(liveData.peers)) {
for (let i = 0; i < this.source.length; i++) {
const peer = this.source[i];
const pubkey = peer.public_key;
const livePeer = liveData.peers.find(p => p.public_key === pubkey);
if (livePeer && livePeer.stats && peer.stats) {
// Instead of making a new object (which triggers grid redraw), mutate the stats deeply
peer.stats.rx_bytes = livePeer.stats.rx_bytes;
peer.stats.tx_bytes = livePeer.stats.tx_bytes;
peer.stats.rx_speed = livePeer.stats.rx_speed;
peer.stats.tx_speed = livePeer.stats.tx_speed;
}
}
}
}
}
});
}
refreshData() {
this.vpnService.getStatus().subscribe({
next: (res) => {
if (res) {
this.status = res;
this.isCommunicationError = res.status === 'failed' && (res.error?.includes('Connection refused') || false);
if (!this.isCommunicationError) {
this.source = (res.peers || []).map(p => ({
...p,
_search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}`
}));
this.computeTotals();
} else {
this.source = [];
}
}
},
error: (err) => console.error("Error refreshing data:", err)
});
}
computeTotals() {
this.totalRx = 0;
this.totalTx = 0;
for (const p of this.source) {
if (p.stats) {
this.totalRx += p.stats.rx_bytes / 1048576; // To MB
this.totalTx += p.stats.tx_bytes / 1048576; // To MB
}
}
const now = new Date();
const timeLabel = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
this.chartData.labels.push(timeLabel);
this.chartData.datasets[0].data.push(this.liveSpeedRx);
this.chartData.datasets[1].data.push(this.liveSpeedTx);
// Keep last 30 intervals (2.5 minutes of history)
if (this.chartData.labels.length > 30) {
this.chartData.labels.shift();
this.chartData.datasets[0].data.shift();
this.chartData.datasets[1].data.shift();
}
// Trigger change detection for chart
this.chartData = { ...this.chartData };
}
show_toast(title: string, body: string, color: string) {
const props = { ...this.toasterForm, color, title, body };
if (this.viewChildren && this.viewChildren.first) {
const componentRef = this.viewChildren.first.addToast(
AppToastComponent,
props,
{}
);
if (componentRef) {
componentRef.instance["closeButton"] = props.closeButton;
}
}
}
// --- Modals & Actions ---
openAddPeerModal() {
this.addPeerModalVisible = true;
this.addPeerStep = 1;
this.editingPeer = false;
this.peerForm = {
pubkey: '', custom_ip: '', name: '', description: '', nat_mode: 'full', split_targets: [''], link_device: false, persistent_keepalive: 25, custom_interface: '',
mt_user: '', mt_pass: '', mt_port: 8728
};
}
addSplitTarget() {
this.peerForm.split_targets.push('');
}
removeSplitTarget(index: number) {
this.peerForm.split_targets.splice(index, 1);
}
trackByIndex(index: number, obj: any): any {
return index;
}
submitPeer() {
// Filter empty splits
const payload = { ...this.peerForm };
payload.split_targets = payload.split_targets.filter((s: string) => s.trim() !== '');
// Scrub credentials if the user unchecked "Link & Manage as MikroTik Device"
if (!payload.link_device) {
payload.mt_user = null;
payload.mt_pass = null;
payload.mt_port = null;
}
const isLinkDevice = payload.link_device;
// Remove the deprecated 'link_device' flag from the API payload entirely
delete payload.link_device;
if (this.editingPeer) {
this.vpnService.editPeer(payload).subscribe({
next: (res) => {
this.show_toast("Success", "Peer updated successfully", "success");
this.addPeerModalVisible = false;
this.refreshData();
},
error: (err) => this.show_toast("Error", err.error?.message || "Failed to update peer", "danger")
});
} else {
this.vpnService.addPeer(payload).subscribe({
next: (res) => {
this.show_toast("Success", "Peer created successfully", "success");
this.addPeerModalVisible = false;
this.refreshData();
// Auto-open appropriate config wizard
if (isLinkDevice) {
this.openScriptModal(res.peer);
} else {
this.openQrModal(res.peer);
}
},
error: (err) => this.show_toast("Error", err.error?.message || "Failed to add peer", "danger")
});
}
}
promptToggleEnabled(item: VpnPeer) {
this.peerToToggle = item;
this.toggleModalVisible = true;
}
confirmToggle() {
if (!this.peerToToggle) return;
const item = this.peerToToggle;
const newState = !item.is_enabled;
this.vpnService.togglePeer(item.public_key, newState).subscribe({
next: () => {
this.show_toast("Success", `Peer ${newState ? 'enabled' : 'disabled'}`, "success");
item.is_enabled = newState;
this.source = [...this.source]; // Force grid update
this.refreshData();
this.toggleModalVisible = false;
this.peerToToggle = null;
},
error: (err) => {
this.show_toast("Error", err.error?.message || "Failed to toggle peer", "danger");
this.toggleModalVisible = false;
this.peerToToggle = null;
}
});
}
openEditModal(item: VpnPeer) {
this.editingPeer = true;
this.addPeerModalVisible = true;
this.addPeerStep = 1;
this.peerForm = {
pubkey: item.public_key,
custom_ip: item.assigned_ip,
name: item.name || '',
description: item.description || '',
nat_mode: item.nat_mode,
split_targets: [...item.split_targets],
link_device: !!item.linked_device_id || !!item.mt_user,
persistent_keepalive: item.persistent_keepalive,
custom_interface: item.custom_interface || '',
mt_user: item.mt_user || '',
mt_pass: item.mt_pass || '',
mt_port: item.mt_port || 8728
};
if (this.peerForm.split_targets.length === 0) this.peerForm.split_targets.push('');
}
scanDevice(item: VpnPeer) {
this.vpnService.scanLinkedDevice(item.public_key).subscribe({
next: (res) => this.show_toast("Success", res.message || "Manual scan initiated asynchronously", "success"),
error: (err) => this.show_toast("Error", err.error?.message || "Scan failed", "danger")
});
}
public scriptModalVisible = false;
public qrModalVisible = false;
// Fetch and show script
openScriptModal(item: VpnPeer) {
this.activePeerConfig = item;
this.configResult = {};
this.scriptModalVisible = true;
this.qrModalVisible = false; // Auto-close QR if open
this.vpnService.getPeerMikrotikScript(item.public_key).subscribe({
next: (res) => {
const rawScript = res.script || '';
// Ensure double-escaped newlines and carriage returns are aggressively parsed to real newlines
this.configResult.script = rawScript.split('\\n').join('\n').replace(/\r\n/g, '\n');
this.scriptModalVisible = true;
},
error: (err) => this.show_toast("Error", "Failed to fetch Mikrotik script", "danger")
});
}
// Fetch and show QR
openQrModal(item: VpnPeer) {
this.activePeerConfig = item;
this.configResult = {};
this.scriptModalVisible = false; // Auto-close Script if open
this.vpnService.getPeerQrCode(item.public_key).subscribe({
next: (blob) => {
const url = URL.createObjectURL(blob);
this.configResult.qrBlobUrl = this.sanitizer.bypassSecurityTrustUrl(url);
this.qrModalVisible = true;
},
error: (err) => this.show_toast("Error", "Failed to fetch QR code", "danger")
});
}
// Download config directly
downloadPeerConfigDirect(item: VpnPeer) {
this.vpnService.getPeerConfig(item.public_key).subscribe({
next: (res) => {
const blob = new Blob([res.config], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${item.assigned_ip || 'peer'}.conf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
error: (err) => this.show_toast("Error", "Failed to fetch config", "danger")
});
}
copyScript() {
if (this.configResult.script) {
navigator.clipboard.writeText(this.configResult.script).then(() => {
this.show_toast("Success", "Copied to clipboard", "success");
});
}
}
confirmDelete(item: VpnPeer) {
this.peerToDelete = item;
this.deleteModalVisible = true;
}
executeDelete() {
if (this.peerToDelete) {
this.vpnService.deletePeer(this.peerToDelete.public_key).subscribe({
next: () => {
this.show_toast("Success", "Peer deleted permanently", "success");
this.deleteModalVisible = false;
this.refreshData();
},
error: (err) => this.show_toast("Error", err.error?.message || "Delete failed", "danger")
});
}
}
openServerConfigModal() {
this.serverConfigModalVisible = true;
this.vpnService.getSystemConfig().subscribe({
next: (res) => this.serverConfig = res.config || {},
error: (err) => this.show_toast("Error", "Failed to fetch config", "danger")
});
}
saveServerConfig() {
this.vpnService.updateSystemConfig(this.serverConfig).subscribe({
next: () => {
this.show_toast("Success", "Server config updated", "success");
this.serverConfigModalVisible = false;
this.refreshData();
},
error: (err) => this.show_toast("Error", err.error?.message || "Update failed", "danger")
});
}
// === COUNTER RESETS ===
promptResetServer() {
this.resetServerModalVisible = true;
}
confirmResetServer() {
this.vpnService.resetServerCounters().subscribe({
next: () => {
this.resetServerModalVisible = false;
this.show_toast('Success', 'Global server traffic counters have been reset.', 'success');
this.totalRx = 0;
this.totalTx = 0;
// Force chart data flush
this.chartData = {
...this.chartData,
labels: [],
datasets: [
{ ...this.chartData.datasets[0], data: [] },
{ ...this.chartData.datasets[1], data: [] }
]
};
},
error: (err) => {
console.error(err);
this.resetServerModalVisible = false;
this.show_toast('Error', 'Failed to reset server counters.', 'danger');
}
});
}
promptResetPeer(peer: VpnPeer) {
this.peerToReset = peer;
this.resetPeerModalVisible = true;
}
confirmResetPeer() {
if (!this.peerToReset || !this.peerToReset.public_key) return;
this.vpnService.resetPeerCounters(this.peerToReset.public_key).subscribe({
next: () => {
this.show_toast('Success', `Counters reset for ${this.peerToReset?.name || this.peerToReset?.assigned_ip}`, 'success');
if (this.peerToReset && this.peerToReset.stats) {
this.peerToReset.stats.rx_bytes = 0;
this.peerToReset.stats.tx_bytes = 0;
this.peerToReset.stats.rx_speed = 0;
this.peerToReset.stats.tx_speed = 0;
}
this.resetPeerModalVisible = false;
this.peerToReset = null;
},
error: (err) => {
console.error(err);
this.resetPeerModalVisible = false;
this.show_toast('Error', `Failed to reset counters for peer.`, 'danger');
this.peerToReset = null;
}
});
}
}

View file

@ -0,0 +1,77 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ChartjsModule } from '@coreui/angular-chartjs';
import { HighlightJsModule } from 'ngx-highlight-js';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { SharedModule as AppSharedModule } from "../../shared/shared.module";
import {
AvatarModule,
ButtonGroupModule,
ButtonModule,
CardModule,
FormModule,
GridModule as CoreUIGridModule,
NavModule,
ProgressModule,
TableModule,
TabsModule,
ModalModule,
DropdownModule,
SharedModule,
ListGroupModule,
BadgeModule,
TooltipModule,
ToastModule
} from '@coreui/angular';
import { MatMenuModule } from '@angular/material/menu';
import { loginChecker } from '../../providers/login_checker';
import { VpnService, VpnStatusResponse, VpnPeer, VpnServerConfig } from '../../providers/mikrowizard/vpn.service';
import { IconModule } from '@coreui/icons-angular';
import { VpnRoutingModule } from './vpn-routing.module';
import { VpnComponent } from './vpn.component';
import { TableModule as PTableModule } from 'primeng/table';
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
import { InputTextModule } from 'primeng/inputtext';
@NgModule({
imports: [
VpnRoutingModule,
CardModule,
NavModule,
IconModule,
TabsModule,
CommonModule,
PTableModule,
PTooltipModule,
InputTextModule,
ProgressModule,
ReactiveFormsModule,
ButtonModule,
FormModule,
ButtonModule,
ButtonGroupModule,
ChartjsModule,
AvatarModule,
TableModule,
ModalModule,
DropdownModule,
SharedModule,
ListGroupModule,
BadgeModule,
TooltipModule,
FormsModule,
ToastModule,
CoreUIGridModule,
MatMenuModule,
HighlightJsModule,
ClipboardModule,
AppSharedModule
],
declarations: [VpnComponent]
})
export class VpnModule {
}

View file

@ -1,116 +1,165 @@
// Here you can add other styles // Here you can add other styles
ngx-super-select.styled>div > span{ ngx-super-select.styled>div>span {
background:var(--cui-input-group-addon-bg, #d8dbe0); background: var(--cui-input-group-addon-bg, #d8dbe0);
height:100%; height: 100%;
padding:8px; padding: 8px;
color:var(--cui-input-group-addon-color, rgba(44, 56, 74, 0.95))!important; color: var(--cui-input-group-addon-color, rgba(44, 56, 74, 0.95)) !important;
} }
ngx-super-select.styled>div { ngx-super-select.styled>div {
padding:0!important; padding: 0 !important;
width:100%; width: 100%;
} }
ngx-super-select.styled{
width:100%; ngx-super-select.styled {
width: 100%;
} }
ngx-super-select.styled.hiden .select-selected-items:before{
content:"click for select" ngx-super-select.styled.hiden .select-selected-items:before {
content: "click for select"
} }
ngx-super-select.styled.hiden .select-selected-items > *{
display:none!important; ngx-super-select.styled.hiden .select-selected-items>* {
display: none !important;
} }
ngx-super-select.styled .selection-counter:before{
content:"Selected Count : " ngx-super-select.styled .selection-counter:before {
content: "Selected Count : "
} }
ngx-super-select.styled .selection-counter{
color:#000!important; ngx-super-select.styled .selection-counter {
background:#aaaaaa26!important; color: #000 !important;
margin:0!important; background: #aaaaaa26 !important;
margin: 0 !important;
} }
.cdk-overlay-container { .cdk-overlay-container {
z-index: 10000; z-index: 10000;
} }
.mdc-text-field--no-label:not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mat-mdc-form-field-infix { .mdc-text-field--no-label:not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mat-mdc-form-field-infix {
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
}
.mat-mdc-form-field-infix {
min-height: 34px;
} }
.mdc-text-field--filled:not(.mdc-text-field--disabled){ .mat-mdc-form-field-infix {
background: transparent!important; min-height: 34px;
}
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
background: transparent !important;
} }
/* Add application styles & imports to this file! */ /* Add application styles & imports to this file! */
pre { pre {
display: flex!important; display: flex !important;
margin-top: 0!important; margin-top: 0 !important;
margin-bottom: 0!important; margin-bottom: 0 !important;
word-wrap: break-word!important; word-wrap: break-word !important;
} }
code { code {
flex: 1!important; flex: 1 !important;
line-height: 1.8em!important; line-height: 1.8em !important;
font-size: 14px!important; font-size: 14px !important;
min-height: 100%!important; min-height: 100% !important;
padding: 1em 1.2em!important; padding: 1em 1.2em !important;
overflow-x: unset!important; overflow-x: unset !important;
overflow-y: unset!important; overflow-y: unset !important;
} }
.hljs.hljs-line-numbers { .hljs.hljs-line-numbers {
padding: 0 !important; padding: 0 !important;
} }
pre .hljs { pre .hljs {
border: none!important; border: none !important;
transition: border ease 1s!important; transition: border ease 1s !important;
} }
.hljs-ln { .hljs-ln {
tr { tr {
&:first-child td { &:first-child td {
padding-top: 10px !important; padding-top: 10px !important;
} }
&:last-child td { &:last-child td {
padding-bottom: 10px !important; padding-bottom: 10px !important;
}
} }
} }
.row-highlighted{ }
z-index: 0!important;
}
.mat-mdc-menu-panel,.mat-mdc-menu-content{
background: transparent!important;
padding: 0!important;
}
/* for block of numbers */
td.hljs-ln-numbers {
position: sticky!important;
left: 0!important;
user-select: none!important;
text-align: center!important;
color: #cccccc6b!important;
border-right: 1px solid #cccccc1c!important;
vertical-align: top!important;
padding-right: 10px !important;
padding-left: 10px !important;
}
/* for block of code */ .row-highlighted {
td.hljs-ln-code { z-index: 0 !important;
padding-left: 10px !important; }
}
.mat-mdc-menu-panel,
.mat-mdc-menu-content {
background: transparent !important;
padding: 0 !important;
}
/* for block of numbers */
td.hljs-ln-numbers {
position: sticky !important;
left: 0 !important;
user-select: none !important;
text-align: center !important;
color: #cccccc6b !important;
border-right: 1px solid #cccccc1c !important;
vertical-align: top !important;
padding-right: 10px !important;
padding-left: 10px !important;
}
/* for block of code */
td.hljs-ln-code {
padding-left: 10px !important;
}
.gui-structure, .gui-structure *{ .gui-structure,
font-size: inherit!important; .gui-structure * {
} font-size: inherit !important;
}
.p-datatable-table-container thead th:first-of-type {
border-top-left-radius: 8px;
}
.p-datatable-table-container thead th:last-of-type {
border-top-right-radius: 8px;
}
.p-datatable-table-container thead th {
padding: 0.30rem 1rem !important;
font-size: 0.7rem !important;
}
.justify-between {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
.table-search-input input {
border-top: none;
border-left: none !important;
border-right: none !important;
border-bottom: 1px !important solid;
border-radius: 0;
padding: 0 5px !important;
}
.table-search-input {
padding-top: 15px;
margin: 0 !important;
}
p-table {
width: 100%
}

View file

@ -26,3 +26,4 @@ $enable-rtl: true;
@import "animations"; @import "animations";
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; @import "~@angular/material/prebuilt-themes/indigo-pink.css";
@import "primeicons/primeicons.css";

View file

@ -4,7 +4,8 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [ "types": [
"@angular/localize" "@angular/localize",
"hammerjs"
], ],
"paths": { "paths": {
"@docs-components/*": ["./src/components/*"] "@docs-components/*": ["./src/components/*"]

View file

@ -14,7 +14,7 @@
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "bundler",
"importHelpers": true, "importHelpers": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
@ -23,7 +23,11 @@
"lib": [ "lib": [
"ES2022", "ES2022",
"dom" "dom"
] ],
"skipLibCheck": true,
"paths": {
"@primeuix/themes/*": ["node_modules/@primeuix/themes/dist/*"]
}
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,