Compare commits

...

7 commits

94 changed files with 10072 additions and 4216 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -5,18 +5,24 @@ export const navItems: INavData[] = [
name: 'Dashboard',
url: '/dashboard',
iconComponent: { name: 'cil-speedometer' },
},
{
name: 'Monitoring Wall',
url: '/monitoring',
icon:'fa-solid fa-tv',
attributes: { 'pro':true }
icon: 'fa-solid fa-tv',
attributes: { 'pro': true }
},
{
title: true,
name: 'Device Managment'
},
{
name: 'WireGuard Server',
url: '/vpn',
icon: 'fa-solid fa-network-wired',
attributes: { 'pro': true }
},
{
name: 'Devices',
url: '/devices',
@ -31,8 +37,8 @@ export const navItems: INavData[] = [
{
name: 'Network Maps',
url: '/maps',
icon:'fa-solid fa-map',
attributes: { 'pro':true }
icon: 'fa-solid fa-map',
attributes: { 'pro': true }
},
// {
// name: 'Tools',
@ -70,18 +76,24 @@ export const navItems: INavData[] = [
url: '/snippets',
icon: 'fa-solid fa-code'
},
{
name: 'Sequences',
url: '/sequences',
icon: 'fa-solid fa-code-branch',
attributes: { 'pro': true }
},
{
name: 'Sync and Cloner',
url: '/cloner',
icon: 'fa-solid fa-rotate',
attributes: { 'pro':true }
attributes: { 'pro': true }
},
{
name: 'Password Vault',
url: '/vault',
icon:'fa-solid fa-vault',
attributes: { 'pro':true }
icon: 'fa-solid fa-vault',
attributes: { 'pro': true }
},
// {
// name: 'Tools',
@ -133,6 +145,12 @@ export const navItems: INavData[] = [
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,
name: 'Users'
@ -140,12 +158,12 @@ export const navItems: INavData[] = [
{
name: 'Users Management',
url: '/user_manager',
icon: 'fa-solid fa-user-gear' ,
icon: 'fa-solid fa-user-gear',
},
{
name: 'Permissions',
url: '/permissions',
icon: 'fa-solid fa-users' ,
icon: 'fa-solid fa-users',
},
{
title: true,
@ -155,7 +173,7 @@ export const navItems: INavData[] = [
{
name: 'Settings',
url: '/settings',
icon: 'fa-solid fa-gear' ,
icon: 'fa-solid fa-gear',
},
// {
// name: 'Backup',
@ -177,7 +195,7 @@ export const navItems: INavData[] = [
{
name: 'Buy Pro',
url: 'https://mikrowizard.com/pricing/',
icon:'fa-solid fa-money-check-dollar',
attributes: { 'free':true,target: '_blank' }
icon: 'fa-solid fa-money-check-dollar',
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

@ -8,7 +8,7 @@ import { User } from './user';
@Injectable()
export class dataProvider {
// public serverUrl: string = "/api";
public serverUrl: string = "";
private db: string = "NothingImportant";
@ -60,60 +60,60 @@ export class dataProvider {
////
//// MikroWizard API
////
get_front_version(){
get_front_version() {
return this.MikroWizardRPC.sendHttpGetRequest("/api/frontver/");
}
change_password(oldpass:string,newpass:string){
var data={
'oldpass':oldpass,
'newpass':newpass
change_password(oldpass: string, newpass: string) {
var data = {
'oldpass': oldpass,
'newpass': newpass
}
return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data);
}
dashboard_stats(versioncheck:boolean,front_version:string){
var data={
'versioncheck':versioncheck,
'front_version':front_version
dashboard_stats(versioncheck: boolean, front_version: string) {
var data = {
'versioncheck': versioncheck,
'front_version': front_version
}
return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data);
}
monitoring_devices_events(page:number,textfilter:string=''){
var data={
'page':page,
'textfilter':textfilter
monitoring_devices_events(page: number, textfilter: string = '') {
var data = {
'page': page,
'textfilter': textfilter
}
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/devs/get", data);
}
monitoring_events_fix(event_id:number){
var data={
'event_id':event_id
monitoring_events_fix(event_id: number) {
var data = {
'event_id': event_id
}
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/fix", data);
}
monitoring_all_events(devid:number,page:number){
var data={
'devid':devid,
'page':page
monitoring_all_events(devid: number, page: number) {
var data = {
'devid': devid,
'page': page
}
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/get", data);
}
monitoring_unfixed_events(devid:number){
var data={
'devid':devid
monitoring_unfixed_events(devid: number) {
var data = {
'devid': devid
}
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/eventunfixed/get", data);
}
dashboard_traffic(delta:string){
var data={
'delta':delta
dashboard_traffic(delta: string) {
var data = {
'delta': delta
}
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);
}
@ -121,442 +121,502 @@ export class dataProvider {
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/list", {});
}
get_devgroup_members(gid:number) {
var data={
'gid':gid
get_devgroup_members(gid: number) {
var data = {
'gid': gid
}
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/members", data);
}
delete_group(id:number){
var data={
'gid':id
delete_group(id: number) {
var data = {
'gid': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/delete", data);
}
delete_devices(devids:any){
delete_devices(devids: any) {
var data = {
'devids':devids
'devids': devids
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/delete", data);
}
get_dev_info(id: number) {
var data={
'devid':id
var data = {
'devid': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/info", data);
}
get_editform(id: number) {
var data={
'devid':id
var data = {
'devid': id
}
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);
}
get_dev_sensors(id: number,delta:string="5m",total_type:string="bps") {
var data={
'devid':id,
'delta':delta,
'total':total_type
get_dev_sensors(id: number, delta: string = "5m", total_type: string = "bps") {
var data = {
'devid': id,
'delta': delta,
'total': total_type
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/sensors", data);
}
get_dev_radio_sensors(id: number, delta:string="5m"){
var data={
'devid':id,
'delta':delta
get_dev_radio_sensors(id: number, delta: string = "5m") {
var data = {
'devid': id,
'delta': delta
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data);
}
get_dev_dhcp_info(id: number){
var data={
'devid':id,
get_dev_dhcp_info(id: number) {
var data = {
'devid': id,
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data);
}
get_dev_ifstat(id: number,delta:string="5m",iface:string="ether1",type:string="bps") {
var data={
'devid':id,
'delta':delta,
'type':type,
'interface':iface
get_dev_ifstat(id: number, delta: string = "5m", iface: string = "ether1", type: string = "bps") {
var data = {
'devid': id,
'delta': delta,
'type': type,
'interface': iface
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/ifstat", data);
}
totp(action:string,userid:string){
var data={
'userid':userid,
'action':action
totp(action: string, userid: string) {
var data = {
'userid': userid,
'action': action
}
return this.MikroWizardRPC.sendJsonRequest("/api/user/totp", data);
}
get_user_restrictions(uid:string){
var data={
'uid':uid
get_user_restrictions(uid: string) {
var data = {
'uid': uid
}
return this.MikroWizardRPC.sendJsonRequest("/api/user/restrictions", data);
}
save_user_restrictions(uid:string,restrictions:any){
var data={
'uid':uid,
'restrictions':restrictions
save_user_restrictions(uid: string, restrictions: any) {
var data = {
'uid': uid,
'restrictions': restrictions
}
return this.MikroWizardRPC.sendJsonRequest("/api/user/save_restrictions", data);
}
mytotp(action:string,otp:any=false){
var data={
'action':action,
'otp':otp
mytotp(action: string, otp: any = false) {
var data = {
'action': action,
'otp': otp
}
return this.MikroWizardRPC.sendJsonRequest("/api/user/mytotp", data);
}
get_auth_logs(filters:any) {
var data=filters;
get_auth_logs(filters: any) {
var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/auth/list", data);
}
get_account_logs(filters:any) {
var data=filters;
get_account_logs(filters: any) {
var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/account/list", data);
}
get_dev_logs(filters:any) {
var data=filters;
get_dev_logs(filters: any) {
var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/list", data);
}
get_syslog(filters:any) {
var data=filters;
get_syslog(filters: any) {
var data = filters;
return this.MikroWizardRPC.sendJsonRequest("/api/syslog/list", data);
}
get_details_grouped(devid:number=0){
var data={
'devid':devid
get_details_grouped(devid: number = 0) {
var data = {
'devid': devid
}
return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/details/list", data);
}
scan_devs(type:string,info:any){
var data: any={
'type':type
scan_devs(type: string, info: any) {
var data: any = {
'type': type
}
if(type=="ip"){
if (type == "ip") {
data = Object.assign(data, info);
}
return this.MikroWizardRPC.sendJsonRequest("/api/scanner/scan", data);
}
scan_results(){
scan_results() {
return this.MikroWizardRPC.sendJsonRequest("/api/scanner/results", {});
}
get_groups(searchstr:string=""){
var data={
'searchstr':searchstr
get_groups(searchstr: string = "") {
var data = {
'searchstr': searchstr
}
return this.MikroWizardRPC.sendJsonRequest("/api/search/groups", data);
}
}
get_devices(searchstr:string=""){
var data={
'searchstr':searchstr
get_devices(searchstr: string = "") {
var data = {
'searchstr': searchstr
}
return this.MikroWizardRPC.sendJsonRequest("/api/search/devices", data);
}
}
update_save_group(group:any){
var data={
update_save_group(group: any) {
var data = {
...group
}
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){
var data={
'name':name,
'description':desc,
'content':content,
'page':page,
'size':size,
'limit':limit
get_snippets(name: string, desc: string, content: string, page: number = 0, size: number = 1000, limit: any = false) {
var data = {
'name': name,
'description': desc,
'content': content,
'page': page,
'size': size,
'limit': limit
}
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/list", data);
}
save_snippet(data:any){
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", {...data});
save_snippet(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", { ...data });
}
Exec_snipet(data:any,members:any) {
data['members']=members;
Exec_snipet(data: any, members: any) {
data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/exec", data);
}
delete_snippet(id:number){
var data={
'id':id
delete_snippet(id: number) {
var data = {
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/delete", data);
}
get_executed_snipet(id:number){
var data={
'id':id
get_executed_snipet(id: number) {
var data = {
'id': id
}
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() {
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/list", {});
}
Add_task(data:any,members:any) {
data['members']=members;
Add_task(data: any, members: any) {
data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/create", data);
}
Delete_task(taskid:Number) {
var data={
'taskid':taskid,
Delete_task(taskid: Number) {
var data = {
'taskid': taskid,
}
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/delete", data);
}
Edit_task(data:any,members:any) {
data['members']=members;
Edit_task(data: any, members: any) {
data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/edit", data);
}
get_task_members(taskid:Number) {
var data={
'taskid':taskid,
get_task_members(taskid: Number) {
var data = {
'taskid': taskid,
}
return this.MikroWizardRPC.sendJsonRequest("/api/taskmember/details", data);
}
get_users(page:Number,size:Number,search:string) {
var data={
'page':page,
'size':size,
'search':search
get_users(page: Number, size: Number, search: string) {
var data = {
'page': page,
'size': size,
'search': search
}
return this.MikroWizardRPC.sendJsonRequest("/api/users/list", data);
}
get_perms(page:Number,size:Number,search:string) {
var data={
'page':page,
'size':size,
'search':search
get_perms(page: Number, size: Number, search: string) {
var data = {
'page': page,
'size': size,
'search': search
}
return this.MikroWizardRPC.sendJsonRequest("/api/perms/list", data);
}
create_perm(name:string,perms:any) {
var data={
'name':name,
'perms':perms
create_perm(name: string, perms: any) {
var data = {
'name': name,
'perms': perms
}
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 = {
'id':id,
'name':name,
'perms':perms
'id': id,
'name': name,
'perms': perms
}
return this.MikroWizardRPC.sendJsonRequest("/api/perms/edit", data);
}
delete_perm(id:number){
var data={
'id':id
delete_perm(id: number) {
var data = {
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/perms/delete", data);
}
get_vault_setting(){
get_vault_setting() {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get", {});
}
vault_task(data:any){
vault_task(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/task", data);
}
vault_history(){
vault_history() {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/history", {});
}
exec_vault(){
exec_vault() {
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/execute", {});
}
reveal_password(devid:number,username:string){
var data={
'devid':devid,
'username':username
reveal_password(devid: number, username: string) {
var data = {
'devid': devid,
'username': username
}
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);
}
get_device_pass(devid:number){
var data={
'devid':devid
get_device_pass(devid: number) {
var data = {
'devid': devid
}
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_device_pass", data);
}
user_perms(uid:string) {
user_perms(uid: string) {
var data = {
'uid':uid,
'uid': uid,
}
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 = {
'uid':uid,
'pid':permid,
'gid':devgroupid
'uid': uid,
'pid': permid,
'gid': devgroupid
}
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/create", data);
}
Delete_user_perm(id:number){
var data={
'id':id
Delete_user_perm(id: number) {
var data = {
'id': id
}
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);
}
delete_user(id:number){
var data={
'uid':id
delete_user(id: number) {
var data = {
'uid': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/user/delete", data);
}
check_firmware(devids:any) {
check_firmware(devids: any) {
var data = {
'devids':devids
'devids': devids
}
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 = {
'page':page,
'size':size,
'search':search
'page': page,
'size': size,
'search': search
}
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data);
}
delete_firm(id:number){
var data={
'id':id
delete_firm(id: number) {
var data = {
'id': id
}
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);
}
get_backup(id:number){
get_backup(id: number) {
var data = {
'id':id
'id': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data);
}
restore_backup(id:number){
restore_backup(id: number) {
var data = {
'backupid':id
'backupid': id
}
return this.MikroWizardRPC.sendJsonRequest("/api/backup/restore", data);
}
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 = {
'version':version
'version': version
}
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 = {
'updatebehavior':updatebehavior,
'firmwaretoinstall':firmwaretoinstall,
'firmwaretoinstallv6':firmwaretoinstallv6
'updatebehavior': updatebehavior,
'firmwaretoinstall': firmwaretoinstall,
'firmwaretoinstallv6': firmwaretoinstallv6
}
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware_settings", data);
}
update_firmware(devids:string){
update_firmware(devids: string) {
var data = {
'devids':devids
'devids': devids
}
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware", data);
}
upgrade_firmware(devids:string){
upgrade_firmware(devids: string) {
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 = {
'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", {});
}
save_sys_setting(data:any){
save_sys_setting(data: any) {
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data);
}
get_running_tasks(){
get_running_tasks() {
return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {});
}
stop_task(signal:number){
var data={
'signal':signal
stop_task(signal: number) {
var data = {
'signal': signal
}
return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data);
}
apply_update(action:string){
var data={
'action':action
apply_update(action: string) {
var data = {
'action': action
}
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data);
}
@ -566,62 +626,66 @@ export class dataProvider {
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {});
}
Add_cloner(data:any,members:any) {
data['members']=members;
Add_cloner(data: any, members: any) {
data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data);
}
Delete_cloner(clonerid:number) {
var data={
'clonerid':clonerid,
Delete_cloner(clonerid: number) {
var data = {
'clonerid': clonerid,
}
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data);
}
Edit_cloner(data:any,members:any) {
data['members']=members;
Edit_cloner(data: any, members: any) {
data['members'] = members;
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data);
}
get_cloner_members(clonerid:number) {
var data={
'clonerid':clonerid,
get_cloner_members(clonerid: number) {
var data = {
'clonerid': clonerid,
}
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data);
}
killSession(devid:number,item:any){
var data={
'devid':devid,
'item':item
killSession(devid: number, item: any) {
var data = {
'devid': devid,
'item': item
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data);
}
getDhcpHistory(item:any){
var data={
'item':item
getDhcpHistory(item: any) {
var data = {
'item': item
}
return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data);
}
getNetworkMap(){
getNetworkMap() {
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 = {
'devices': devices
}
return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add", data);
}
bulk_add_status(taskId: string){
bulk_add_status(taskId: string) {
var data = {
'taskId': taskId
}
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 = {
'groupId': groupId,
'action': action
@ -637,7 +701,7 @@ export class dataProvider {
this.MikroWizardRPC.clearCookeis();
this.MikroWizardRPC.setNewSession(context, session);
}
checkSessionExpired(error: any) {
console.log(error);
if ('title' in error && error.title == "session_expired")

View file

@ -1,6 +1,6 @@
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
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-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-row>
<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
*ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate"
style="color: #74C0FC;"></i>
@ -14,8 +14,10 @@
</h5>
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
{{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']">
<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 color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
*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-col>
<c-col xs [lg]="1">
@ -73,49 +75,143 @@
</c-col>
</div>
</c-row>
<gui-grid [rowDetail]="rowDetail" [source]="source" [columnMenu]="columnMenu" [paging]="paging"
[sorting]="sorting" [infoPanel]="infoPanel" [autoResizeWidth]=true>
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
<ng-template let-value="item.index" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Device Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
{{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="Action" field="action">
<ng-template let-value="item.action" let-item="item" let-index="index">
<div>{{value}}</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Section" field="section">
<ng-template let-value="item.section" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Date" field="created">
<ng-template let-value="item.created" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Message" field="message" [enabled]="false">
<ng-template let-value="item.message" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
</gui-grid>
<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 commands/users..."
(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]="['username', 'name', 'address', 'config', 'section', 'action']" selectionMode="single"
(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="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>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>
</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 { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiRowDetail,
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { formatInTimeZone } from "date-fns-tz";
@ -26,10 +15,14 @@ import { formatInTimeZone } from "date-fns-tz";
})
export class AccComponent implements OnInit {
@Input() component_devid: any=false;
public uid: number;
public uname: string;
public tz: string;
public filterText: string;
public uid!: number;
public uname!: string;
public tz!: string;
public filterText!: string;
public detailsVisible: boolean = false;
public selectedLog: any = null;
@ViewChild('dt') table!: Table;
public reloading: boolean = false;
public filters: any = {
devid: false,
@ -75,78 +68,20 @@ export class AccComponent implements OnInit {
}
}
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public Selectedrows: any;
public selected_rows: any[] = [];
public Selectedrows: any[] = [];
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 = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
showLogDetails(log: any) {
this.selectedLog = log;
this.detailsVisible = true;
}
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,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
reinitgrid(field: string, $event: any) {
if (field == "start") this.filters["start_time"] = $event.target.value;
@ -170,9 +105,10 @@ export class AccComponent implements OnInit {
this.initGridTable();
}
OnDestroy(): void {}
onSelectedRows(rows: Array<GuiSelectedRow>): void {
this.rows = rows;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
onSelectionChange(value: any[]) {
this.selected_rows = value;
this.Selectedrows = value.map(item => item.id);
this.rows = value;
}
removefilter(filter: any) {

View file

@ -13,7 +13,9 @@ import {
import { AccRoutingModule } from "./acc-routing.module";
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 { MatInputModule } from "@angular/material/input";
@ -32,7 +34,9 @@ import { FormsModule } from "@angular/forms";
ButtonModule,
FormModule,
ButtonModule,
GuiGridModule,
TableModule,
DrawerModule,
InputTextModule,
CollapseModule,
MatFormFieldModule,
MatInputModule,

View file

@ -1,6 +1,6 @@
<c-row>
<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-row>
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
@ -14,8 +14,10 @@
</h5>
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
{{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']">
<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 color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
*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-col>
<c-col xs [lg]="1">
@ -95,64 +97,180 @@
</div>
</c-row>
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [autoResizeWidth]=true>
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
<ng-template let-value="item.index" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<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>
<div class="mb-3 d-flex justify-content-end gap-2">
<span class="p-input-icon-left table-search-input">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<gui-grid-column header="Time/Msg" field="duration">
<ng-template let-value="item.duration" let-item="item" let-index="index">
<span *ngIf="item.ltype!='failed'">{{value}}</span>
<span *ngIf="item.ltype=='failed'">{{item.message}}</span>
<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]="['username', 'name', 'ip', 'devip', 'by']" [(selection)]="selected_rows"
(onRowSelect)="showLogDetails($event.data)" selectionMode="single">
</ng-template>
</gui-grid-column>
<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="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 let-value="item.ltype" let-item="item.id" let-index="index">
<c-badge color="success" *ngIf="value=='loggedin'"> Logged In</c-badge>
<c-badge color="warning" *ngIf="value=='loggedout'"> Logged Out</c-badge>
<c-badge color="danger" *ngIf="value=='failed'"> Failed</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Date" field="created">
<ng-template let-value="item.created" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
</gui-grid>
<ng-template pTemplate="body" let-item>
<tr [pSelectableRow]="item" style="cursor: pointer;">
<td class="text-center">{{item.index}}</td>
<td>
<i *ngIf="item.stype=='local'" pTooltip="local user" style="color: #ff2a00; margin-right: 3px;"
class="fa-solid fa-user-tie"></i>
<i *ngIf="item.stype=='radius'" pTooltip="radius user" style="color: #096114; margin-right: 3px;"
class="fa-solid fa-server"></i>
{{item.name}}
</td>
<td>{{item.devip}}</td>
<td>{{item.username}}</td>
<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>
</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 { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { formatInTimeZone } from "date-fns-tz";
interface IUser {
@ -37,10 +27,14 @@ interface IUser {
})
export class AuthComponent implements OnInit {
@Input() component_devid: any=false;
public uid: number;
public uname: string;
public uid!: number;
public uname!: string;
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 reloading: boolean = false;
public filters: any = {
@ -87,42 +81,19 @@ export class AuthComponent implements OnInit {
}
}
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
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 = {
enabled: true,
multiSorting: true,
};
showLogDetails(log: any) {
this.selectedLog = log;
this.detailsVisible = 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,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
reinitgrid(field: string, $event: any) {
if (field == "start") this.filters["start_time"] = $event.target.value;
@ -179,9 +150,10 @@ export class AuthComponent implements OnInit {
}
this.initGridTable();
}
onSelectedRows(rows: Array<GuiSelectedRow>): void {
this.rows = rows;
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
onSelectionChange(value: any[]) {
this.selected_rows = value;
this.Selectedrows = value.map(item => item.id);
this.rows = value;
}
removefilter(filter: any) {

View file

@ -11,7 +11,10 @@ import {
} from '@coreui/angular';
import { AuthRoutingModule } from './auth-routing.module';
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';
@ -27,7 +30,10 @@ import {MatSelectModule} from '@angular/material/select';
GridModule,
FormsModule,
ButtonModule,
GuiGridModule,
TableModule,
DrawerModule,
InputTextModule,
TooltipModule,
CollapseModule,
MatFormFieldModule,
MatInputModule,

View file

@ -20,15 +20,14 @@
<i class="fa-solid fa-code-compare me-1"></i>{{compareitems.length}} Selected
</c-badge>
<div class="selected-items d-flex gap-1">
<c-badge color="secondary" *ngFor="let item of compareitems; index as i"
<c-badge color="secondary" *ngFor="let item of compareitems; index as i"
class="selected-item d-flex align-items-center">
<span class="me-1">{{item.devname}}</span>
<i class="fa-solid fa-times cursor-pointer" (click)="delete_compare(i)"
<i class="fa-solid fa-times cursor-pointer" (click)="delete_compare(i)"
title="Remove from comparison"></i>
</c-badge>
</div>
<button *ngIf="compareitems.length > 1" (click)="start_compare()"
cButton color="success" size="sm">
<button *ngIf="compareitems.length > 1" (click)="start_compare()" cButton color="success" size="sm">
<i class="fa-solid fa-code-compare me-1"></i>Compare
</button>
<button (click)="clearAllCompare()" cButton color="secondary" size="sm" variant="outline">
@ -39,7 +38,7 @@
<!-- Filter Toggle -->
<button (click)="toggleCollapse()" cButton color="primary" variant="outline">
<i class="fa-solid fa-filter me-1"></i>Filters
<i class="fa-solid" [class]="filters_visible ? 'fa-chevron-up' : 'fa-chevron-down'"
<i class="fa-solid" [class]="filters_visible ? 'fa-chevron-up' : 'fa-chevron-down'"
style="margin-left: 0.5rem; font-size: 0.8rem;"></i>
</button>
</div>
@ -84,8 +83,8 @@
<i class="fa-solid fa-magnifying-glass me-1 text-primary"></i>Config Search
</label>
<mat-form-field appearance="outline" class="w-100">
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']"
matInput placeholder="Search in configurations...">
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']" matInput
placeholder="Search in configurations...">
</mat-form-field>
</div>
</c-col>
@ -93,65 +92,104 @@
</c-card-body>
</c-card>
</div>
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]=true>
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
<ng-template let-value="item.index" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Device Name" field="devname">
<ng-template let-value="item.devname" let-item="item" let-index="index">
{{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="backup Time" field="createdC">
<ng-template let-value="item.createdC" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="File Size" field="filesize">
<ng-template let-value="item.filesize" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="MAC" field="devmac" [enabled]="false">
<ng-template let-value="item.devmac" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" field="id" width="280">
<ng-template let-value="item.id" let-item="item" let-index="index">
<div class="d-flex gap-1">
<button cButton [disabled]="backuploading" color="primary" size="sm"
(click)="ShowBackup(item)" variant="outline" title="View backup content">
<i *ngIf="backuploading && currentBackup?.id === item.id"
class="fa-solid fa-spinner fa-spin me-1"></i>
<i *ngIf="!backuploading || currentBackup?.id !== item.id"
class="fa-solid fa-eye me-1"></i>
View
</button>
<button *ngIf="ispro" cButton color="info" size="sm" variant="outline"
(click)="add_for_compare(item)" title="Add to comparison"
[disabled]="isInCompareList(item)">
<i class="fa-solid fa-code-compare me-1"></i>
{{isInCompareList(item) ? 'Added' : 'Compare'}}
</button>
<button *ngIf="ispro" cButton color="warning" size="sm" variant="outline"
(click)="restore_backup(false, false, item)" title="Restore this backup">
<i class="fa-solid fa-rotate-left me-1"></i>
Restore
</button>
</div>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 backups..."
(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]="['devname', 'devip', 'createdC', 'filesize']" [loading]="loading">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="index" style="width: 70px" class="text-center" pResizableColumn>
<div class="justify-between">
<span>#No</span>
<p-sortIcon field="index"></p-sortIcon>
</div>
</th>
<th pSortableColumn="devname" pResizableColumn>
<div class="justify-between">
<span>Device Name</span>
<span>
<p-sortIcon field="devname"></p-sortIcon>
<p-columnFilter type="text" field="devname" 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="createdC" pResizableColumn>
<div class="justify-between">
<span>Backup Time</span>
<span>
<p-sortIcon field="createdC"></p-sortIcon>
<p-columnFilter type="text" field="createdC" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="filesize" pResizableColumn>
<div class="justify-between">
<span>File Size</span>
<span>
<p-sortIcon field="filesize"></p-sortIcon>
<p-columnFilter type="text" field="filesize" 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 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>
</c-col>
@ -166,11 +204,12 @@
<!-- <pre style="height: 100%;">
<code *ngIf="!loading" language="yaml" style="height:70vh" [highlight]="codeForHighlightAuto"
lineNumbers></code></pre> -->
<div highlight-js [lang]="hlang" [options]="{}" >{{codeForHighlightAuto}}</div>
<div highlight-js [lang]="hlang" [options]="{}">{{codeForHighlightAuto}}</div>
</c-modal-body>
<c-modal-footer style="justify-content: space-between;">
<button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null" (click)="copy_this()" cButton color="secondary">
<button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null"
(click)="copy_this()" cButton color="secondary">
<i class="fa-regular fa-copy"></i> To clipboard
</button>
<div>
@ -230,7 +269,7 @@
<i class="fa-solid fa-rotate-left fa-3x text-warning mb-3"></i>
<h6>Restore Configuration Backup</h6>
</div>
<div class="backup-info bg-light p-3 rounded mb-3">
<h6 class="mb-2"><i class="fa-solid fa-info-circle me-2 text-primary"></i>Backup Details</h6>
<div class="row">
@ -276,7 +315,7 @@
<div class="confirmation-box bg-light border border-danger p-3 rounded">
<p class="mb-3 fw-bold">To proceed with this critical action, type <code>CONFIRM</code> in the box below:</p>
<input cFormControl [(ngModel)]="confirmationText" placeholder="Type CONFIRM to proceed"
<input cFormControl [(ngModel)]="confirmationText" placeholder="Type CONFIRM to proceed"
class="form-control text-center fw-bold" style="letter-spacing: 2px;" />
</div>
@ -290,8 +329,7 @@
<button (click)="cancelCriticalRestore()" cButton color="secondary">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
<button (click)="restore_backup(true, true)" cButton color="danger"
[disabled]="confirmationText !== 'CONFIRM'">
<button (click)="restore_backup(true, true)" cButton color="danger" [disabled]="confirmationText !== 'CONFIRM'">
<i class="fa-solid fa-rotate-left me-1"></i>RESTORE BACKUP
</button>
</c-modal-footer>

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 { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { formatInTimeZone } from "date-fns-tz";
import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component";
@ -22,10 +12,10 @@ import { AppToastComponent } from "../toast-simple/toast.component";
styleUrls: ["backups.component.scss"],
})
export class BackupsComponent implements OnInit {
public uid: number;
public uname: string;
public uid: number = 0;
public uname: string = '';
public tz: string = "UTC";
public filterText: string;
public filterText: string = '';
public filters: any = {};
public codeForHighlightAuto: string = "";
public ispro: boolean = false;
@ -69,34 +59,19 @@ export class BackupsComponent implements OnInit {
return value !== undefined && value !== null && value !== "";
}
}
@ViewChild("dt") table!: Table;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public backuploading : boolean=false;
public backuploading: boolean = false;
public rows: any = [];
public Selectedrows: any;
public BakcupModalVisible: boolean = false;
public devid: number = 0;
public filters_visible: boolean = false;
public currentBackup:any=false;
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,
};
public currentBackup: any = false;
public hlang: string = '';
toasterForm = {
autohide: true,
@ -105,25 +80,10 @@ export class BackupsComponent implements OnInit {
fade: true,
closeButton: true,
};
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,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
ngOnInit(): void {
this.devid = Number(this.route.snapshot.paramMap.get("devid"));

View file

@ -17,7 +17,9 @@ import {
import { BackupsRoutingModule } from "./backups-routing.module";
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 { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field";
@ -35,7 +37,9 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
FormModule,
FormsModule,
ButtonModule,
GuiGridModule,
TableModule,
InputTextModule,
TooltipModule,
CollapseModule,
BadgeModule,
AlertModule,

View file

@ -1,3 +1,4 @@
<div style="position: relative;">
<c-row>
<c-col xs>
<c-card class="mb-4">
@ -13,107 +14,172 @@
</c-row>
</c-card-header>
<c-card-body>
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [autoResizeWidth]=true>
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<strong>{{value}}</strong>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Description" field="description">
<ng-template let-value="item.description" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Direction" field="direction">
<ng-template let-value="item.direction" let-item="item" let-index="index">
<c-badge [color]="value == 'twoway' ? 'success' : 'warning'">{{value == 'twoway' ? 'Two Way' : 'Master Mode'}}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Live Mode" field="live_mode">
<ng-template let-value="item.live_mode" let-item="item" let-index="index">
<c-badge [color]="value ? 'success' : 'secondary'">{{value ? 'Active' : 'Inactive'}}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Schedule" field="desc_cron">
<ng-template let-value="item.desc_cron" 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)="editAddCloner(item,'edit');"><i
class="fa-regular fa-pen-to-square"></i></button>
<!-- <button cButton color="info" size="sm" (click)="confirm_run(item);" class="mx-1"><i
class="fa-solid fa-bolt"></i></button> -->
<button class=" mx-1" cButton color="danger" size="sm" (click)="confirm_delete(item);"><i
class="fa-regular fa-trash-can"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 cloner..." (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', 'description', 'direction']" [loading]="loading">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<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="description" pResizableColumn>
<div class="justify-between">
<span>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="direction" pResizableColumn>
<div class="justify-between">
<span>Direction</span>
<span>
<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>
</c-col>
</c-row>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<c-modal #EditClonerModal backdrop="static" size="lg" [(visible)]="EditClonerModalVisible" id="EditClonerModal">
<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']=='add'" cModalTitle><i class="fa-solid fa-plus me-2"></i>Add New Cloner</h5>
<button [cModalToggle]="EditClonerModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-3">
<!-- Basic Information -->
<div class="cloner-form-section mb-3">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
</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>
<c-modal #EditClonerModal backdrop="static" size="lg" [(visible)]="EditClonerModalVisible" id="EditClonerModal">
<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']=='add'" cModalTitle><i class="fa-solid fa-plus me-2"></i>Add New Cloner</h5>
<button [cModalToggle]="EditClonerModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-3">
<!-- Basic Information -->
<div class="cloner-form-section mb-3">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
</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 -->
<div class="cloner-form-section mb-3">
<div class="section-header mb-2">
<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>
<!-- Sync Configuration -->
<div class="cloner-form-section mb-3">
<div class="section-header mb-2">
<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>
<!-- Peers Configuration (Currently disabled - only devices supported) -->
<!-- <div class="cloner-form-section mb-3">
<!-- Peers Configuration (Currently disabled - only devices supported) -->
<!-- <div class="cloner-form-section mb-3">
<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>
<small class="text-muted">Select the type of peers to synchronize</small>
@ -129,233 +195,301 @@
</c-row>
</div> -->
<!-- Commands Configuration -->
<div class="cloner-form-section mb-3">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-terminal me-2"></i>Commands Configuration</h6>
</div>
<div class="commands-container-compact">
<div class="nav nav-underline commands-nav-compact">
<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>
</div>
<!-- Commands Configuration -->
<div class="cloner-form-section mb-3">
<div class="section-header mb-2">
<h6 class="section-title mb-0"><i class="fa-solid fa-terminal me-2"></i>Commands Configuration</h6>
</div>
<div class="commands-container-compact">
<div class="nav nav-underline commands-nav-compact">
<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>
</div>
<c-tab-content class="command-sections-compact" #tabContent="cTabContent">
<c-tab-pane *ngFor="let tab of tabs; let i = index">
<div class="command-category-compact" *ngFor="let section of tab.sections">
<h6 class="category-title-compact">{{ section.title }}</h6>
<div class="commands-grid-compact">
<div class="command-item-compact" *ngFor="let command of section.commands">
<div class="command-content-compact">
<span class="command-name-compact">{{ command }}</span>
<div class="custom-switch-compact">
<input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)" (click)="activate_command(command)" [id]="command.replace('/', '')">
<label class="custom-control-label" [for]="command.replace('/', '')"></label>
</div>
</div>
<c-tab-content class="command-sections-compact" #tabContent="cTabContent">
<c-tab-pane *ngFor="let tab of tabs; let i = index">
<div class="command-category-compact" *ngFor="let section of tab.sections">
<h6 class="category-title-compact">{{ section.title }}</h6>
<div class="commands-grid-compact">
<div class="command-item-compact" *ngFor="let command of section.commands">
<div class="command-content-compact">
<span class="command-name-compact">{{ command }}</span>
<div class="custom-switch-compact">
<input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)"
(click)="activate_command(command)" [id]="command.replace('/', '')">
<label class="custom-control-label" [for]="command.replace('/', '')"></label>
</div>
</div>
</div>
</div>
</c-tab-pane>
</c-tab-content>
</div>
</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>
<!-- 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 class="peers-container">
<div *ngIf="SelectedMembers.length == 0" class="empty-peers">
<div class="empty-icon">
<i class="fa-solid fa-users-slash fa-2x text-muted opacity-50"></i>
</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 class="empty-text">
<strong>No peers added</strong>
<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 class="peers-container">
<div *ngIf="SelectedMembers.length == 0" class="empty-peers">
<div class="empty-icon">
<i class="fa-solid fa-users-slash fa-2x text-muted opacity-50"></i>
</div>
<div class="empty-text">
<strong>No peers added</strong>
<p class="mb-0 text-muted">Click "Add Members" to start adding peers</p>
</div>
</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 *ngIf="SelectedMembers.length > 0">
<p-table [value]="SelectedMembers" [paginator]="true" [rows]="10" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Name</span>
<p-sortIcon field="name"></p-sortIcon>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
<c-badge color="secondary">{{value}}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action" align="center">
<ng-template let-value="item.id" let-item="item" let-index="index">
<div class="btn-group" role="group">
<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">
</th>
<th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="mac" pResizableColumn>
<div class="justify-between">
<span>MAC Address</span>
<p-sortIcon field="mac"></p-sortIcon>
</div>
</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="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>
</button>
<button cButton color="danger" size="sm" variant="outline"
[cTooltip]="'Remove Member'" (click)="remove_member(item)">
<i class="fa-solid fa-times"></i>
<button cButton color="danger" size="sm" variant="outline" pTooltip="Remove Member"
(click)="remove_member(item)">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
<div class="add-members-section mt-3">
<button cButton color="success" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add Members
</button>
</div>
</td>
</tr>
</ng-template>
<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">
<button cButton color="success" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add Members
</button>
</div>
</div>
</c-modal-body>
<c-modal-footer>
<button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
<button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
<button [cModalToggle]="EditClonerModal.id" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>
</div>
</c-modal-body>
<c-modal-footer>
<button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
<button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
<button [cModalToggle]="EditClonerModal.id" cButton color="secondary">
Close
</button>
</c-modal-footer>
</c-modal>
<c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<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>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4">
<!-- Selection Summary -->
<div class="mb-3" style="min-height: 58px;">
<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>
<span><strong>{{NewMemberRows.length}}</strong> {{SelectedCloner['pair_type']}} selected for addition</span>
</c-alert>
</div>
<c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<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>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4">
<!-- Selection Summary -->
<div class="mb-3" style="min-height: 58px;">
<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>
<span><strong>{{NewMemberRows.length}}</strong> {{SelectedCloner['pair_type']}} selected for addition</span>
</c-alert>
</div>
<!-- Available Members -->
<c-card>
<c-card-header class="bg-light">
<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>
Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total)
</h6>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="availbleMembers.length == 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i>
<h6>All {{SelectedCloner['pair_type']}} are already added</h6>
<p class="mb-0">No available {{SelectedCloner['pair_type']}} to add to this cloner</p>
<!-- Available Members -->
<c-card>
<c-card-header class="bg-light">
<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>
Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total)
</h6>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="availbleMembers.length == 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i>
<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 *ngIf="availbleMembers.length > 0">
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [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 me-2 text-primary" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'" [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
<strong>{{value}}</strong>
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50]" [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="justify-between">
<span>Name</span>
<p-sortIcon field="name"></p-sortIcon>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
<c-badge color="secondary">{{value}}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
<small class="text-muted">{{value}}</small>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
</c-card-body>
</c-card>
</c-modal-body>
</th>
<th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="ip" pResizableColumn>
<div class="justify-between">
<span>IP Address</span>
<p-sortIcon field="ip"></p-sortIcon>
</div>
</th>
<th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="mac" pResizableColumn>
<div class="justify-between">
<span>MAC Address</span>
<p-sortIcon field="mac"></p-sortIcon>
</div>
</th>
</tr>
</ng-template>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">Select {{SelectedCloner['pair_type']}} from the list above to add them to the cloner</small>
</div>
<div>
<button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success">
<i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} {{SelectedCloner['pair_type'] | titlecase}}
</button>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</div>
</c-modal-footer>
</c-modal>
<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 me-2 text-primary" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'"
[class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
<strong>{{item.name}}</strong>
</div>
</td>
<td *ngIf="SelectedCloner['pair_type']=='devices'">
<c-badge color="secondary">{{item.ip}}</c-badge>
</td>
<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-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!
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">Select {{SelectedCloner['pair_type']}} from the list above to add them to the
cloner</small>
</div>
<div>
<button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success">
<i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} {{SelectedCloner['pair_type'] | titlecase}}
</button>
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info">
Close
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</c-modal-footer>
</c-modal>
</div>
</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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
@ -27,10 +16,13 @@ import { AppToastComponent } from "../toast-simple/toast.component";
})
export class ClonerComponent implements OnInit {
public uid: number;
public uname: string;
public uid: number = 0;
public uname: string = '';
public ispro: boolean = false;
@ViewChild('dt') table!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
constructor(
private data_provider: dataProvider,
private router: Router,
@ -62,7 +54,6 @@ export class ClonerComponent implements OnInit {
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public SelectedCloner: any = {};
@ -74,9 +65,17 @@ export class ClonerComponent implements OnInit {
public NewMemberModalVisible: boolean = false;
public availbleMembers: any = [];
public NewMemberRows: any = [];
public SelectedNewMemberRows: any;
public SelectedNewMemberRows: any[] = [];
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=[
{
"name": "Network",
@ -186,59 +185,13 @@ export class ClonerComponent implements OnInit {
}
];
public sorting = {
enabled: true,
multiSorting: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
options: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Snippet",
searchEnabled: true,
enableDarkMode: false,
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
toasterForm = {
autohide: true,
delay: 3000,
position: "fixed",
fade: true,
closeButton: true,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
applyFilterGlobalNewMembers($event: any, stringVal: string) {
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
activate_command(command:string){
// add to active_commands if it not added before
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.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
this.SelectedNewMemberRows = rows;
}
add_new_members() {

View file

@ -18,9 +18,12 @@ import {
} from "@coreui/angular";
import { ClonerRoutingModule } from "./cloner-routing.module";
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 { SharedModule } from "../../shared/shared.module";
@NgModule({
imports: [
@ -31,7 +34,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule,
ReactiveFormsModule,
FormsModule,
@ -42,6 +47,7 @@ import { NgxSuperSelectModule} from "ngx-super-select";
TabsModule,
BadgeModule,
AlertModule,
SharedModule,
],
declarations: [ClonerComponent],
providers: [TitleCasePipe],

View file

@ -142,98 +142,179 @@
</form>
</c-col>
</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 col-12" style="height: 250px;"></div>
<div class="placeholder col-12" style="height: 230px;"></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-card-body>
</c-card>
<c-row>
<c-col xl="6" lg="12" class="h-100" style="min-height: 160px!important;display: grid">
<c-card class="mb-1 p-1 h-100" style="padding-left: 5px!important;">
<div class="my-1">
<h4 style="display: inline-block;">Version and Serial information</h4>
</div>
<div *ngIf="!stats">
<div class="placeholder-glow">
<div class="placeholder col-8"></div>
<div class="placeholder col-6"></div>
<div class="placeholder col-10"></div>
<div class="placeholder col-7"></div>
<c-row class="align-items-stretch">
<c-col xl="6" lg="12" class="d-flex flex-column mb-3 mb-xl-0" style="min-height: 160px!important;">
<c-card class="flex-grow-1 border-0 shadow-sm"
style="margin: 0 !important; overflow: hidden; background: linear-gradient(to bottom right, #ffffff, #f8f9fa);">
<c-card-body class="d-flex flex-column justify-content-between p-2 px-3">
<!-- Skeleton loader -->
<div *ngIf="!stats">
<div class="placeholder-glow">
<div class="placeholder col-8 mb-2"></div>
<div class="placeholder col-6 mb-2"></div>
<div class="placeholder col-10 mb-2"></div>
<div class="placeholder col-7"></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">
<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>
<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> -->
<div *ngIf="stats" class="d-flex flex-column h-100">
<!-- Header / Serial -->
<div class="d-flex justify-content-between align-items-center mb-2">
<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-col>
<c-col xl="6" lg="12" class="h-100" style="min-height: 160px!important;display: grid;">
<c-card class="h-100" style="padding: 0!important;margin: 0!important;">
<c-col xl="6" lg="12" class="d-flex flex-column" style="min-height: 160px!important;">
<c-card class="flex-grow-1" style="padding: 0!important;margin: 0!important;">
<div *ngIf="!stats" style="padding: 20px;">
<div class="placeholder-glow">
<div class="placeholder col-4" style="height: 150px; float: left; margin-right: 20px;"></div>

View file

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

View file

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

View file

@ -24,9 +24,6 @@
cursor: pointer;
}
.interfaces gui-grid gui-structure{
min-height: unset!important;
}
.nav-underline .nav-link:hover,.nav-underline .nav-link:focus {
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 { dataProvider } from "../../providers/mikrowizard/data";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { __setFunctionName } from "tslib";
interface IUser {
name: string;
@ -38,10 +28,10 @@ type radiodata = {
encapsulation: ViewEncapsulation.None,
})
export class DeviceComponent implements OnInit, OnDestroy {
public uid: number;
public uid!: number;
public sessionloaded: boolean = false;
public uname: string;
public tz: string;
public uname!: string;
public tz!: string;
public ispro: boolean = false;
public small_screen=false;
public show_dev_logs: boolean = false;
@ -81,8 +71,11 @@ export class DeviceComponent implements OnInit, OnDestroy {
}
public devdata: any;
public devsensors: any;
public radio_devsensors: radiodata;
public columns: Array<GuiColumn> = [];
public radiodata!: radiodata;
public radio_devsensors!: any;
@ViewChild('dtInterfaces') dtInterfaces!: Table;
public loading: boolean = true;
public radio_loading: boolean = true;
public InterfaceChartModalVisible: boolean = false;
@ -98,45 +91,11 @@ export class DeviceComponent implements OnInit, OnDestroy {
public dhcp_server_available: boolean = false;
public dhcp_server_data: any = {};
public sorting = {
enabled: true,
multiSorting: true,
};
public interfaces: Array<any> = [];
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 20,
pageSizes: [20],
display: GuiPagingDisplay.ADVANCED,
};
public columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: true,
};
objectlen(object:any){
return Object.keys(object).length;
applyFilterGlobal($event: any, stringVal: string) {
this.dtInterfaces.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
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(){
this.get_DHCP_data();
}
@ -553,6 +512,12 @@ export class DeviceComponent implements OnInit, OnDestroy {
show_history(itme:any) {
return
}
objectlen(object:any){
return object ? Object.keys(object).length : 0;
}
strangth_at_rate_extract(data:string){
return data ? data.split(',') : [];
}
ngOnDestroy() {
clearInterval(this.data_interval);
}

View file

@ -22,7 +22,9 @@ import { ChartjsModule } from "@coreui/angular-chartjs";
import { DeviceRoutingModule } from "./device-routing.module";
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 { DeviceInfoModule } from "./device-info/device-info.module";
@ -53,7 +55,9 @@ import { AccModule } from "../acc_log/acc.module";
AuthModule,
AccModule,
SpinnerModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
NavbarModule,
ModalModule,
TableModule,

View file

@ -1,5 +1,5 @@
<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-row *ngIf="small_screen">
<c-col sm="6" style="padding-right: 0;">
@ -77,51 +77,101 @@
<c-col xl="9" sm="12">
<c-card class="mb-1 h-100">
<c-card-body>
<gui-grid [source]="item['lease_table']" [columnMenu]="columnMenu" [sorting]="sorting"
[searching]="searching" [infoPanel]="infoPanel" [rowHeight]="28" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="address" field="address">
<ng-template let-value="item.address" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Type" field="dynamic">
<ng-template let-value="item" let-item="item" let-index="index">
<span *ngIf="item['dynamic']">Dynamic</span>
<span *ngIf="item['static']">Static</span>
</ng-template>
</gui-grid-column>
<gui-grid-column header="expire" field="expires-after">
<ng-template let-value="item['expires-after']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="host-name" field="host-name">
<ng-template let-value="item['host-name']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="last-seen" field="last-seen">
<ng-template let-value="item['last-seen']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="status" field="status">
<ng-template let-value="item['status']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="MAC" field="mac-address">
<ng-template let-value="item['mac-address']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="60" field="">
<ng-template let-value="item" let-item="item" let-index="index">
<button cButton color="info" size="sm" (click)="show_history(item)" class="mx-1"><i
class="fa-solid fa-clock-rotate-left"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 leases..."
(input)="applyFilterGlobalLeases($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dtLeases [value]="item['lease_table']" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['address', 'host-name', 'mac-address', 'status']">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="address" pResizableColumn>
<div class="justify-between">
<span>Address</span>
<span>
<p-sortIcon field="address"></p-sortIcon>
<p-columnFilter type="text" field="address" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="dynamic" pResizableColumn>
<div class="justify-between">
<span>Type</span>
<span>
<p-sortIcon field="dynamic"></p-sortIcon>
<p-columnFilter type="boolean" field="dynamic" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="expires-after" pResizableColumn>
<div class="justify-between">
<span>Expire</span>
<span>
<p-sortIcon field="expires-after"></p-sortIcon>
<p-columnFilter type="text" field="expires-after" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="host-name" pResizableColumn>
<div class="justify-between">
<span>Host Name</span>
<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>
</c-col>
@ -129,36 +179,60 @@
</ng-container>
<c-modal #DhcpHistoryModal backdrop="static" size="xl" [visible]="dhcp_history_modal"
id="DhcpHistoryModal" *ngIf="dhcp_history_modal">
<c-modal #DhcpHistoryModal backdrop="static" size="xl" [visible]="dhcp_history_modal" id="DhcpHistoryModal"
*ngIf="dhcp_history_modal">
<c-modal-header>
<h5 cModalTitle>History for {{current_dhcp['mac-address']}}</h5>
<button [cModalToggle]="DhcpHistoryModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body style="overflow-x: auto;">
<gui-grid style="min-width: 900px;" [source]="dhcp_history" [columnMenu]="columnMenu" [sorting]="sorting"
[searching]="searching" [infoPanel]="infoPanel" [rowHeight]="35" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="comment" field="comment" >
<ng-template let-value="item.comment" let-item="item" let-index="index">
<div style="text-wrap: auto;font-weight: bold;line-height: normal;">{{value}}</div>
<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 history..."
(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>
</gui-grid-column>
<gui-grid-column header="type" field="detail" width="150">
<ng-template let-value="item['detail']" let-item="item" let-index="index">
{{value}}
<ng-template pTemplate="body" let-hist>
<tr>
<td>
<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>
</gui-grid-column>
<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>
</p-table>
</c-modal-body>
<c-modal-footer>
<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 { formatInTimeZone } from "date-fns-tz";
import { Table } from 'primeng/table';
@Component({
selector: 'app-dhcp-info',
@ -11,13 +12,11 @@ import { formatInTimeZone } from "date-fns-tz";
export class DhcpInfoComponent implements AfterContentInit {
@Input() dhcp_server_data: any;
@Input() small_screen: boolean=false;
@Input() columnMenu: any;
@Input() sorting: any;
@Input() searching: any;
@Input() infoPanel: any;
@Input() paging: any;
@Input() rowSelectionMode: any;
@Input() tz: any;
@ViewChild('dtLeases') dtLeases!: Table;
@ViewChild('dtHistory') dtHistory!: Table;
dhcp_history:any;
dhcp_history_modal: boolean = false;
current_dhcp:any;
@ -26,7 +25,14 @@ export class DhcpInfoComponent implements AfterContentInit {
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;
this.data_provider.getDhcpHistory(item).then((res) => {
_self.current_dhcp = item;

View file

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

View file

@ -9,7 +9,8 @@
{{ping['successful_pings']}}</c-badge></h6>
<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>
<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>
<table cTable small>
<thead>

View file

@ -4,7 +4,7 @@
<c-card-header>
<c-row>
<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
*ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate"
style="color: #74C0FC;"></i>
@ -14,8 +14,10 @@
</h5>
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
{{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']">
<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 color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
*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-col>
<c-col xs [lg]="1">
@ -84,74 +86,207 @@
</div>
</c-row>
<gui-grid wid [rowDetail]="rowDetail" [horizontalGrid]="true" [rowHeight]="52" [source]="source"
[columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]="true">
<gui-grid-column header="#No" type="NUMBER" field="index" width="1" align="CENTER">
<ng-template let-value="item.index" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
<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 logs..." (input)="applyFilterGlobal($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<gui-grid-column header="level" width='90' wid field="level">
<ng-template let-value="item.level" let-item="item" let-index="index">
<c-badge style="cursor: pointer; font-weight: normal" color="danger" *ngIf="value == 'Critical'">{{ value
}}</c-badge>
<c-badge style="cursor: pointer; font-weight: normal" color="warning" *ngIf="value == 'Error'">{{ value
}}</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">
<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]="['detail', 'comment', 'src', 'name', 'devip', 'level']" selectionMode="single"
(onRowSelect)="showLogDetails($event.data)">
<div class="gui-dev-info">
{{ value }}
<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="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>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Source" width='90' field="src">
<ng-template let-value="item.src" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
<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 class="px-4 pb-4">
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-desktop me-2 text-primary"></i>Device Info</h5>
<div class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">Device Name:</div>
<div class="col-7 fw-bold">{{selectedLog.name}}</div>
</div>
<div *ngIf="!item.fixtime"> {{ value }}</div>
</ng-template>
</gui-grid-column>
<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 class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">IP Address:</div>
<div class="col-7 font-monospace">{{selectedLog.devip}}</div>
</div>
</ng-template>
</gui-grid-column>
</gui-grid>
<div class="row mb-2">
<div class="col-5 fw-bold text-muted small text-uppercase">MAC Address:</div>
<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>
</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 { dataProvider } from "../../providers/mikrowizard/data";
import { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiRowDetail,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { formatInTimeZone } from "date-fns-tz";
import { takeUntil } from "rxjs/operators";
import { Subject } from "rxjs";
@ -27,10 +17,14 @@ import { Subject } from "rxjs";
})
export class DevLogsComponent implements OnInit {
@Input() component_devid: any=false;
public uid: number;
public uname: string;
public uid!: number;
public uname!: string;
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 = {
start_time: false,
end_time: false,
@ -73,108 +67,28 @@ export class DevLogsComponent implements OnInit {
}
}
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public Selectedrows: any;
public selected_rows: any[] = [];
public Selectedrows: any[] = [];
public devid: number = 0;
public sorting = {
enabled: true,
multiSorting: true,
};
public bankMultiFilterCtrl: FormControl = new FormControl<string>("");
protected _onDestroy = new Subject<void>();
public campaignOnestart: any;
public campaignOneend: any;
rowDetail: GuiRowDetail = {
enabled: true,
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>
<table>
<tr>
<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>`;
},
};
getSeverityColor(level: string): string {
if (level === "Critical") return "#e55353";
if (level === "Warning") return "#f9b115";
return "#3399ff";
}
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
showLogDetails(log: any) {
this.selectedLog = log;
this.detailsVisible = true;
}
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,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
ngOnInit(): void {
var _self = this;
if (this.component_devid) {

View file

@ -15,7 +15,10 @@ import {
import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
import { DevLogsRoutingModule } from "./devlogs-routing.module";
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 { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field";
@ -34,7 +37,10 @@ import { MatSelectModule } from "@angular/material/select";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
TableModule,
DrawerModule,
InputTextModule,
TooltipModule,
CollapseModule,
BadgeModule,
MatInputModule,

View file

@ -8,22 +8,24 @@
</c-col>
<c-col xs [lg]="9">
<h6 style="text-align: right;">
<button cButton color="success" (click)="openAddDeviceModal()" class="mx-1"
size="sm" style="color: #fff;"><i class="fa-solid fa-plus"></i> Bulk Add </button>
<button cButton color="success" (click)="openAddDeviceModal()" class="mx-1" size="sm"
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"
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"
size="sm" style="color: #fff;"><i class="fa-solid fa-history"></i> History</button>
<button cButton color="primary" (click)="show_exec()" class="mx-1" size="sm" style="color: #fff;"><i
class="fa-solid fa-history"></i> History</button>
</h6>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<c-row>
<c-row>
<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 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>
<button cButton color="secondary" class="mx-1" size="sm" (click)="clearFilter()">Clear Filter</button>
</c-col>
@ -38,116 +40,162 @@
</c-input-group>
</c-col>
</c-row>
<gui-grid #grid [rowClass]="rowClass" [source]="source" [searching]="searching" [paging]="paging"
[columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection"
(selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true>
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<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="Tooltip text"
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>
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="CPU Type" field="arch">
<ng-template let-value="item.arch" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Firmware" field="current_firmware">
<ng-template let-value="item.current_firmware" let-item="item" let-index="index">
<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 class="mb-1 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 devices..."
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[(selection)]="selected_rows" (selectionChange)="onSelectionChange($event)"
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['name','ip','mac','arch']">
<ng-template pTemplate="header">
<tr>
<th style="width: 3rem" pResizableColumn>
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
</th>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Name</span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</div>
</mat-menu>
</ng-template>
</gui-grid-column>
</gui-grid>
</th>
<th pSortableColumn="arch" pResizableColumn>
<div class="justify-between">
<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-container [fluid]="true">
<a cNavbarBrand href="javascript:;">
@ -292,54 +340,105 @@
</select>
</c-input-group>
</div>
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching"
[source]="filteredExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [paging]="paging">
<gui-grid-column header="Type" field="task_type" [width]="100">
<ng-template let-value="item['task_type']" let-item="item" let-index="index">
<i *ngIf="item.task_type === 'ip-scan'" class="fas fa-search" style="margin-right: 3px; color: #0d6efd;"></i>
<i *ngIf="item.task_type === 'bulk-add'" class="fas fa-plus-circle" style="margin-right: 3px; color: #198754;"></i>
<span style="font-size: 11px;">{{getTaskTypeLabel(value)}}</span>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Time" field="time" [width]="220">
<ng-template let-value="value" let-item="item" let-index="index">
<div style="font-size: 10px; line-height: 1.3;">
<div><strong>Start:</strong> {{item.started}}</div>
<div><strong>End:</strong> {{item.ended}}</div>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Details" field="details">
<ng-template let-value="value" let-item="item" let-index="index">
<div *ngIf="item.task_type === 'ip-scan'" style="font-size: 12px; line-height: 1.3;">
<div>{{item.start_ip}} - {{item.end_ip}}</div>
<div>User: {{item.username}}</div>
</div>
<div *ngIf="item.task_type === 'bulk-add'" style="font-size: 12px; line-height: 1.3;">
<div>{{item.device_count}} devices</div>
<div style="word-break: break-all;">{{item.task_id}}</div>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Results" field="results" align="center" [width]="70">
<ng-template let-value="value" let-item="item" let-index="index">
<div style="font-size: 11px;">
<span style="color: green;">✓ {{item.success_count}}</span>
<span *ngIf="item.failed_count > 0" style="color: red; margin-left: 5px;">✗ {{item.failed_count}}</span>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" field="actions" align="center" [width]="160">
<ng-template let-value="value" let-item="item" let-index="index">
<button (click)="showTaskDetails(item)" color="info" size="sm" cButton class="me-1">
<i class="fas fa-eye"></i> Details
</button>
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton>
<i class="fas fa-download"></i> CSV
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 history..."
(input)="dtTask.filterGlobal($any($event.target).value, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dtTask [value]="filteredExecutedData" [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]="['task_id', 'username', 'start_ip', 'end_ip']">
<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="name" pResizableColumn>
<div class="justify-between">
<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="createdC" pResizableColumn>
<div class="justify-between">
<span>Execution Time</span>
<span>
<p-sortIcon field="createdC"></p-sortIcon>
<p-columnFilter type="text" field="createdC" display="menu" class="ms-auto" />
</span>
</div>
</th>
<th pSortableColumn="username" pResizableColumn>
<div class="justify-between">
<span>User</span>
<span>
<p-sortIcon field="username"></p-sortIcon>
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
</span>
</div>
</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-footer>
<button (click)="ExecutedDataModalVisible=!ExecutedDataModalVisible" cButton color="secondary">
@ -453,7 +552,8 @@
<c-modal-body>
<div *ngIf="addDeviceStep === 1">
<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">
<div *ngIf="csvPreview.length > 0">
<h6>Preview (First 3 rows):</h6>
@ -522,7 +622,8 @@
</div>
</c-modal-body>
<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
</button>
<button *ngIf="addDeviceStep === 3" cButton color="success" (click)="closeAddDeviceModal()">
@ -557,13 +658,14 @@
</c-modal>
<!-- Task Details Modal -->
<c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal"
style="z-index: 1060; backdrop-filter: blur(2px);">
<c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal"
style="z-index: 1060; backdrop-filter: blur(2px);">
<c-modal-header style="background: rgba(255,255,255,0.95); backdrop-filter: blur(10px);">
<h5 cModalTitle>{{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results</h5>
<button (click)="closeDetailsModal()" cButtonClose></button>
</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">
<c-row>
<c-col md="6">
@ -587,8 +689,8 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Detailed Results:</h6>
<div class="col-md-4">
<input type="text" class="form-control form-control-sm" placeholder="Search IP or error..."
[(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()">
<input type="text" class="form-control form-control-sm" placeholder="Search IP or error..."
[(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()">
</div>
</div>
<table class="table table-striped table-hover table-responsive">
@ -620,19 +722,20 @@
<nav *ngIf="getTotalDetailsPages() > 1" class="mt-3">
<ul class="pagination pagination-sm justify-content-center">
<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
</button>
</li>
<li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index"
[class.active]="detailsCurrentPage === i + 1">
<li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index"
[class.active]="detailsCurrentPage === i + 1">
<button class="page-link" (click)="onDetailsPageChange(i + 1)">
{{i + 1}}
</button>
</li>
<li class="page-item" [class.disabled]="detailsCurrentPage === getTotalDetailsPages()">
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)"
[disabled]="detailsCurrentPage === getTotalDetailsPages()">
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)"
[disabled]="detailsCurrentPage === getTotalDetailsPages()">
Next
</button>
</li>

View file

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

View file

@ -22,7 +22,11 @@ import {
import { MatMenuModule } from "@angular/material/menu";
import { DevicesRoutingModule } from "./devices-routing.module";
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({
imports: [
@ -34,7 +38,6 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
NavbarModule,
CollapseModule,
DropdownModule,
@ -46,6 +49,11 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
MatMenuModule,
TooltipModule,
TableModule,
PrimeNGTableModule,
CheckboxModule,
InputTextModule,
MultiSelectModule,
PrimeNGTooltipModule
],
declarations: [DevicesComponent],
})

View file

@ -13,82 +13,119 @@
</c-row>
</c-card-header>
<c-card-body>
<gui-grid [source]="source" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]=true>
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<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>
<div class="mb-1 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 groups..." (input)="applyFilterGlobal($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<gui-grid-column header="Users" field="assigned_users" width="80" align="CENTER">
<ng-template let-value="item.assigned_users" let-item="item" let-index="index">
<c-badge color="info"
[cTooltip]="getUsersTooltip(value)"
[cTooltipPlacement]="'top'"
style="cursor: help">
<i class="fa-solid fa-users me-1"></i>{{value.length}}
</c-badge>
</ng-template>
</gui-grid-column>
<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']" [loading]="loading">
<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_members(item.id)"
style="border: none;padding: 4px 7px;"><i class="fa-regular fa-eye"></i><small> View devices</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)="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>
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Name</span>
<span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</span>
</div>
</mat-menu>
</ng-template>
</gui-grid-column>
</gui-grid>
</th>
<th pSortableColumn="description" pResizableColumn>
<div class="justify-between">
<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>
@ -134,30 +171,54 @@
<p class="mb-0">Click "Add Devices" to start adding devices to this group</p>
</div>
<div *ngIf="groupMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [searching]="searching" [source]="groupMembers" [columnMenu]="columnMenu"
[sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection"
(selectedRows)="onSelectedRowsMembers($event)" [paging]="paging">
<gui-grid-column header="Device 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-server me-2 text-primary"></i>
<strong>{{value}}</strong>
</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">
<c-badge color="secondary">{{value}}</c-badge>
</ng-template>
</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 color="danger" size="sm" variant="outline" (click)="remove_from_group(item.id)" title="Remove from group">
<i class="fa-solid fa-times"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 members..."
(input)="applyFilterMembers($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dtMembers [value]="groupMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" styleClass="p-datatable-sm" [(selection)]="MemberRows"
(selectionChange)="onSelectedRowsMembers($event)" [globalFilterFields]="['name', 'ip']">
<ng-template pTemplate="header">
<tr>
<th style="width: 4rem"><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Name</span>
<p-sortIcon field="name"></p-sortIcon>
</div>
</th>
<th pSortableColumn="mac" pResizableColumn>
<div class="justify-between">
<span>MAC Address</span>
<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>
</c-card-body>
</c-card>
@ -172,7 +233,8 @@
</c-modal-footer>
</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">
<h5 cModalTitle><i class="fa-solid fa-plus-circle me-2"></i>Add Devices to Group</h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
@ -189,7 +251,8 @@
<!-- Available Devices -->
<c-card>
<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-body class="p-0">
<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>
</div>
<div *ngIf="availbleMembers.length > 0">
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging">
<gui-grid-column header="Device 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-server me-2 text-primary"></i>
<strong>{{value}}</strong>
</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">
<c-badge color="secondary">{{value}}</c-badge>
</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">
<small class="text-muted">{{value}}</small>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 available..."
(input)="applyFilterNewMembers($event, 'contains')" class="form-control-sm" />
</span>
</div>
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [resizableColumns]="true"
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped" [(selection)]="NewMemberRows"
(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>Device Name <p-sortIcon
field="name"></p-sortIcon><p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</th>
<th pSortableColumn="ip" pResizableColumn>IP Address <p-sortIcon field="ip"></p-sortIcon><p-columnFilter
type="text" field="ip" display="menu" class="ms-auto" /></th>
<th pSortableColumn="mac" pResizableColumn>MAC Address <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="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>
</c-card-body>
</c-card>
@ -268,7 +348,8 @@
</button>
</c-modal-footer>
</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>
<h5 cModalTitle><i class="fa-solid fa-users-gear me-2"></i>User Permissions - {{selectedGroup?.name}}</h5>
<button [cModalToggle]="UserManagementModal.id" cButtonClose></button>
@ -283,18 +364,11 @@
<c-col md="4">
<div class="search-select-wrapper">
<label class="search-label">User</label>
<input cFormControl
[(ngModel)]="userSearch"
(input)="filterUsers($event)"
(focus)="showUserDropdown = true"
(blur)="hideUserDropdown()"
placeholder="Search and select user..."
class="search-input compact-select"
autocomplete="off" />
<input cFormControl [(ngModel)]="userSearch" (input)="filterUsers($event)"
(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 *ngFor="let user of filteredUsers"
class="search-option"
(mousedown)="selectUser(user)">
<div *ngFor="let user of filteredUsers" class="search-option" (mousedown)="selectUser(user)">
{{user.username}} ({{user.first_name}} {{user.last_name}})
</div>
</div>
@ -306,28 +380,24 @@
<c-col md="4">
<div class="search-select-wrapper">
<label class="search-label">Permission</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" />
<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"
<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">
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch"
class="search-no-results">
No permissions found
</div>
</div>
</c-col>
<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
</button>
</c-col>
@ -336,7 +406,8 @@
</c-card>
<c-card>
<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-body class="p-0">
<div *ngIf="selectedGroup?.assigned_users?.length === 0" class="text-center p-4 text-muted">
@ -369,14 +440,12 @@
</td>
<td>
<div class="btn-group" role="group">
<button cButton color="warning" size="sm" variant="outline"
(click)="editUserPermission(user)" title="Change Permission"
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<button cButton color="warning" size="sm" variant="outline" (click)="editUserPermission(user)"
title="Change Permission" [disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<i class="fa-solid fa-edit"></i>
</button>
<button cButton color="danger" size="sm" variant="outline"
(click)="removeUserPermission(user)" title="Remove Permission"
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<button cButton color="danger" size="sm" variant="outline" (click)="removeUserPermission(user)"
title="Remove Permission" [disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<i class="fa-solid fa-trash"></i>
</button>
</div>
@ -402,7 +471,8 @@
</c-modal-header>
<c-modal-body>
<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 class="mb-3">
<label class="form-label">New Permission</label>
@ -430,7 +500,9 @@
<c-modal-body>
<div class="text-center">
<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>
</div>
</c-modal-body>
@ -451,13 +523,17 @@
<c-modal-body>
<div class="text-center">
<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 class="text-muted small">This action will affect all devices in this group and may take some time to complete.</p>
<p>Are you sure you want to <strong>{{firmwareAction}}</strong> firmware for all devices in group
<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>
</c-modal-body>
<c-modal-footer>
<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 [cModalToggle]="FirmwareConfirmModal.id" cButton color="secondary">
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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import { formatInTimeZone } from "date-fns-tz";
import {
GuiSearching,
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
interface IUser {
name: string;
@ -35,9 +24,13 @@ interface IUser {
styleUrls: ["devgroup.component.scss"]
})
export class DevicesGroupComponent implements OnInit {
public uid: number;
public uname: string;
public tz: string;
public uid: number = 0;
public uname: string = '';
public tz: string = '';
@ViewChild('dt') table!: Table;
@ViewChild('dtMembers') tableMembers!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
constructor(
private data_provider: dataProvider,
@ -68,7 +61,6 @@ export class DevicesGroupComponent implements OnInit {
}
}
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public MemberRows: any = [];
public NewMemberRows: any = [];
@ -117,42 +109,17 @@ export class DevicesGroupComponent implements OnInit {
id: 0,
name: "",
};
public sorting = {
enabled: true,
multiSorting: true,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
applyFilterMembers($event: any, stringVal: string) {
this.tableMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
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,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
applyFilterNewMembers($event: any, stringVal: string) {
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
ngOnInit(): void {
this.initGridTable();
@ -175,13 +142,13 @@ export class DevicesGroupComponent implements OnInit {
});
}
onSelectedRowsMembers(rows: Array<GuiSelectedRow>): void {
onSelectedRowsMembers(rows: any[]): void {
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.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source.id);
this.SelectedNewMemberRows = rows.map((m: any) => m.id);
}
add_new_members() {
var _self = this;
@ -193,7 +160,7 @@ export class DevicesGroupComponent implements OnInit {
this.groupMembers = [
...new Set(
this.groupMembers.concat(
this.NewMemberRows.map((m: GuiSelectedRow) => m.source)
this.NewMemberRows
)
),
];

View file

@ -15,7 +15,9 @@ import {
} from "@coreui/angular";
import { DevicesGroupRoutingModule } from "./devgroup-routing.module";
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 { FormsModule } from "@angular/forms";
import { MatMenuModule } from "@angular/material/menu";
@ -30,7 +32,9 @@ import { MatMenuModule } from "@angular/material/menu";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
CollapseModule,
ModalModule,
BadgeModule,

View file

@ -1,10 +1,28 @@
<div style="position: relative;">
<c-row class="network-container">
<c-col [xs]="12" class="network-col">
<c-card class="network-card">
<button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<div #network class="network-canvas"></div>
<div class="map-actions">
<button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn">
<i class="fas fa-sync-alt"></i> Refresh
</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-col>
</c-row>
@ -80,3 +98,21 @@
<button cButton color="secondary" (click)="closeWebAccessModal()">Cancel</button>
</c-modal-footer>
</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;
}
.refresh-btn {
.map-actions {
position: absolute;
top: 10px;
right: 10px;
right: 15px;
z-index: 1000;
display: flex;
gap: 8px;
align-items: center;
}
.refresh-btn {
padding: 6px 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 {
@ -34,7 +45,49 @@
border-radius: 8px;
border: 1px solid #dee2e6;
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 {
cursor: default;

View file

@ -10,7 +10,7 @@ import { DataSet } from 'vis-data';
templateUrl: "maps.component.html",
styleUrls: ["maps.component.scss"],
})
export class MapsComponent implements OnInit {
export class MapsComponent implements OnInit, OnDestroy {
public uid: number;
public uname: string;
public ispro: boolean = false;
@ -19,8 +19,12 @@ export class MapsComponent implements OnInit {
public savedPositionsKey = "network-layout";
public selectedDevice: any = null;
public showWebAccessModal: boolean = false;
public showMoreInfoModal: boolean = false;
public currentDeviceInfo: any = null;
public showResetModal: boolean = false;
public loadingMap: boolean = false;
private pollingTimer: any;
private isDestroyed: boolean = false;
constructor(
private data_provider: dataProvider,
private router: Router,
@ -54,13 +58,38 @@ export class MapsComponent implements OnInit {
this.loadNetworkData();
}
ngOnDestroy(): void {
this.isDestroyed = true;
if (this.pollingTimer) {
clearTimeout(this.pollingTimer);
}
}
loadNetworkData(): void {
if (this.isDestroyed) return;
clearTimeout(this.pollingTimer);
this.loadingMap = true;
this.data_provider.getNetworkMap().then((res) => {
this.mikrotikData = res;
console.dir(res);
setTimeout(() => {
this.createNetworkMap();
}, 100);
if (this.isDestroyed) return;
// Normalize response - handle array or object with 'result' property
const data = (res && res.result) ? res.result : res;
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
}
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 { ClipboardModule } from "@angular/cdk/clipboard";
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { SharedModule } from "../../shared/shared.module";
@NgModule({
imports: [
@ -53,7 +54,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
TooltipModule,
UtilitiesModule,
InfiniteScrollModule,
IconModule
IconModule,
SharedModule
],
declarations: [MapsComponent],
})

View file

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

View file

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

View file

@ -13,30 +13,66 @@
</c-row>
</c-card-header>
<c-card-body>
<gui-grid [rowHeight]="82" [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu"
[sorting]="sorting" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column width="auto" header="Perms" field="perms">
<ng-template let-value="item.role" let-item="item" let-index="index">
<div style="text-wrap: initial;">
<ng-container *ngFor="let perm of item['perms'] | keyvalue">
<c-badge *ngIf="perm.value" class="m-1" color="success">{{perm.key}}</c-badge>
</ng-container>
</div>
</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" class="mx-1" (click)="editAddTask(item,'edit');"><i
class="fa-regular fa-pen-to-square"></i></button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item);"><i
class="fa-regular fa-trash-can"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
<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 permissions..." (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']"
[loading]="loading">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
<span>Permission Name</span>
<span>
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</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>
</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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component";
@ -30,8 +25,10 @@ interface IUser {
templateUrl: "permissions.component.html",
})
export class PermissionsComponent implements OnInit {
public uid: number;
public uname: string;
public uid: number = 0;
public uname: string = '';
@ViewChild('dt') table!: Table;
constructor(
private data_provider: dataProvider,
@ -63,7 +60,6 @@ export class PermissionsComponent implements OnInit {
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public SelectedPerm: any = {};
@ -107,24 +103,9 @@ export class PermissionsComponent implements OnInit {
closeButton: true,
};
public sorting = {
enabled: true,
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,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
show_toast(title: string, body: string, color: string) {
const { ...props } = { ...this.toasterForm, color, title, body };
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 { 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({
imports: [
@ -33,7 +35,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
TableModule,
InputTextModule,
TooltipModule,
ModalModule,
FormsModule,
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 {
display: none!important;
display: none !important;
}
/* Settings Container */
.settings-container {
max-width: 1200px;
margin: 0 auto;
}
@ -24,7 +23,7 @@
}
.settings-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
// background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #dee2e6;
padding: 1.25rem 1.5rem;
}
@ -92,12 +91,12 @@
padding: 1.5rem;
transition: all 0.2s ease;
}
.section-card:hover {
border-color: #0a58ca;
background: linear-gradient(135deg, #cfe2ff 0%, #e7f3ff 100%);
}
.section-header {
display: flex;
align-items: center;
@ -205,7 +204,8 @@
/* Form Controls */
.form-control, .form-select {
.form-control,
.form-select {
border-radius: 6px;
border: 1px solid #ced4da;
padding: 0.5rem 0.75rem;
@ -213,7 +213,8 @@
transition: all 0.2s ease;
}
.form-control:focus, .form-select:focus {
.form-control:focus,
.form-select:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
@ -250,29 +251,30 @@
/* Mobile Responsive for Informative Elements */
@media (max-width: 768px) {
.info-banner,
.warning-banner,
.success-banner {
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
}
.help-icon {
font-size: 0.75rem;
}
.setting-help {
font-size: 0.75rem;
}
.switch-description {
font-size: 0.8rem;
}
.switch-description small {
font-size: 0.7rem;
}
::ng-deep .tooltip-inner {
max-width: 250px;
font-size: 0.75rem;
@ -280,6 +282,7 @@
}
@media (max-width: 576px) {
.info-banner,
.warning-banner,
.success-banner {
@ -287,7 +290,7 @@
align-items: flex-start;
gap: 0.25rem;
}
.setting-label {
flex-direction: column;
align-items: flex-start;
@ -300,40 +303,40 @@
.settings-header {
padding: 1rem;
}
.settings-tabs {
padding: 0 1rem;
}
.tab-content {
padding: 1rem;
}
.tab-link {
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
.status-section,
.config-section,
.firmware-settings {
padding: 1rem;
}
.download-section .section-card {
padding: 1rem;
}
.switch-content {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.switch-info {
margin-right: 0;
}
.section-title {
font-size: 1rem;
}
@ -343,15 +346,15 @@
.settings-container {
margin: 0 0.5rem;
}
.settings-card {
border-radius: 8px;
}
.tab-content {
padding: 0.75rem;
}
.status-section,
.config-section,
.firmware-settings {
@ -421,8 +424,15 @@
/* Animation for dropdown */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.search-dropdown {
@ -618,16 +628,4 @@
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Grid Enhancements */
::ng-deep .gui-grid {
.gui-grid-header {
background: #f8f9fa;
font-weight: 600;
}
.gui-grid-cell {
padding: 0.75rem 0.5rem;
}
}

View file

@ -4,21 +4,12 @@ import {
QueryList,
ViewChildren,
ViewEncapsulation,
ViewChild
} from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSelectedRow,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { ToasterComponent } from "@coreui/angular";
import { AppToastComponent } from "../toast-simple/toast.component";
import { TimeZones } from "./timezones-data";
@ -30,14 +21,16 @@ import { TimeZones } from "./timezones-data";
})
export class SettingsComponent implements OnInit {
public uid: number;
public uname: string;
public uid: number = 0;
public uname: string = '';
public ispro:boolean=false;
public filterText: string;
public filterText: string = '';
public filters: any = {};
public firms: any = {};
public firmtodownload: any = {};
public activeTab: string = 'firmware';
@ViewChild('dt') dt!: Table;
// Search functionality properties
public firmwareSearch: string = '';
@ -81,7 +74,6 @@ export class SettingsComponent implements OnInit {
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public SysConfigloading: boolean = true;
@ -104,37 +96,9 @@ export class SettingsComponent implements OnInit {
closeButton: true,
};
public sorting = {
enabled: true,
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,
};
applyFilterGlobal($event: any, stringVal: string) {
this.dt.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
public timezones = this.TimeZones.timezones;
ngOnInit(): void {
@ -208,9 +172,9 @@ export class SettingsComponent implements OnInit {
});
}
onSelectedRows(rows: Array<GuiSelectedRow>): void {
onSelectedRows(rows: any[]): void {
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) {

View file

@ -15,7 +15,9 @@ import {
} from "@coreui/angular";
import { SettingsRoutingModule } from "./settings-routing.module";
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";
@ -29,7 +31,9 @@ import { FormsModule } from "@angular/forms";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
SpinnerModule,
ToastModule,
ModalModule,

View file

@ -8,46 +8,71 @@
</c-col>
<c-col xs [lg]="9">
<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>
</h6>
</c-col>
</c-row>
</c-card-header>
<c-card-body>
<gui-grid [source]="source" [searching]="searching" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true>
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
<i *ngIf="item.status=='updated'" style="color: green;margin: 5px;" class="fa-solid fa-check"></i>
<i *ngIf="item.status=='failed'" style="color: red;margin: 5px;" class="fa-solid fa-x"></i>
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Description" field="description">
<ng-template let-value="item.description" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Created" field="created">
<ng-template let-value="item.created" let-item="item" let-index="index">
<div>{{value}}</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" field="action" align="center">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="primary" size="sm" (click)="Edit_Snippet(item,'edit')" class=""><i
class="fa-regular fa-pen-to-square mx-1"></i>Edit</button>
<button cButton color="warning" size="sm" (click)="Run_Snippet(item,'exec')" class="mx-1"><i
class="fa-solid fa-bolt mx-1"></i>Execute</button>
<button cButton color="info" size="sm" (click)="show_exec(item)" class="mx-1"><i
class="fa-solid fa-bolt mx-1"></i>Data</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)" class=""><i
class="fa-regular fa-trash-can mx-1"></i>Delete</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
[(selection)]="Selectedrows" [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true"
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['name','description']">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
<div class="justify-between">
Name
<p-sortIcon field="name"></p-sortIcon>
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
</div>
</th>
<th pSortableColumn="description" pResizableColumn>
<div class="justify-between">
Description
<p-sortIcon field="description"></p-sortIcon>
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
</div>
</th>
<th pSortableColumn="created" pResizableColumn>
<div class="justify-between">
Created
<p-sortIcon field="created"></p-sortIcon>
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
</div>
</th>
<th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<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>
</c-col>
@ -63,7 +88,8 @@
</c-modal-header>
<c-modal-body>
<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>
</div>
@ -84,30 +110,36 @@
</c-input-group>
<h5>Members :</h5>
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [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">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="MAC" field="mac">
<ng-template let-value="item.mac" 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="danger" size="sm" (click)="remove_member(item)"><i
class="fa-regular fa-trash-can"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
<p-table #dtSelected [value]="SelectedMembers" [paginator]="true" [rows]="5" [resizableColumns]="true"
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>Name <p-sortIcon field="name"></p-sortIcon></th>
<th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="mac" pResizableColumn>MAC
<p-sortIcon field="mac"></p-sortIcon>
</th>
<th style="width: 80px" class="text-center" pResizableColumn>Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-member>
<tr>
<td>{{member.name}}</td>
<td *ngIf="current_snippet['selection_type']=='devices'">{{member.mac}}</td>
<td class="text-center">
<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 />
<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 (click)="submit('exec')" cButton color="primary">Execute</button>
<button [cModalToggle]="ExecSnipetModal.id" cButton color="secondary">
Close
</button>
@ -123,25 +155,48 @@
<c-modal-body>
<c-input-group class="mb-3">
<h5>Group Members :</h5>
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true
[paging]="paging">
<gui-grid-column header="Member Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" 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 *ngIf="current_snippet['selection_type']=='devices'" 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>
<div class="mb-3 d-flex justify-content-end w-100">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search..." (input)="applyFilterNewMember($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<p-table #dtNewMember [value]="availbleMembers" [paginator]="true" [rows]="10"
[(selection)]="SelectedNewMemberRows" (selectionChange)="NewMemberRows = $event" [resizableColumns]="true"
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
Class="p-datatable-sm p-datatable-gridlines p-datatable-striped" [globalFilterFields]="['name','ip','mac']">
<ng-template pTemplate="header">
<tr>
<th style="width: 3rem" pResizableColumn>
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
</th>
<th pSortableColumn="name" pResizableColumn>
Name
<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 />
</c-input-group>
<hr />
@ -164,25 +219,44 @@
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching"
[source]="ExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]=true
[paging]="paging">
<gui-grid-column header="Start time" field="start">
<ng-template let-value="item['started']" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column header="End time" field="end">
<ng-template let-value="item['ended']" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="info" field="mac" align="center">
<ng-template let-value="item['result']" let-item="item" let-index="index">
<button (click)="exportToCsv(value)" color="primary" cButton>download</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<div class="mb-3 d-flex justify-content-end w-100">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text" pInputText placeholder="Search history..." (input)="applyFilterHistory($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<p-table #dtHistory width="100%" [value]="ExecutedData" [paginator]="true" [rows]="10" [resizableColumns]="true"
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['username','detail']">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="started" pResizableColumn>
Start time
<p-sortIcon field="started"></p-sortIcon>
<p-columnFilter type="text" field="started" display="menu" class="ms-auto" />
</th>
<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 />
</c-input-group>
<hr />
@ -210,16 +284,19 @@
</c-input-group>
<c-input-group 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>
</div>
</c-input-group>
<c-input-group class="mb-3">
<div [cFormFloating]="true" class="mb-3">
<textarea [style.height.px]="50 + (23 * lineNum)"
cFormControl (ngModelChange)="calcline($event)" id="floatingInput" placeholder="Snippet code" [(ngModel)]="current_snippet['content']" ></textarea>
<textarea [style.height.px]="50 + (23 * lineNum)" cFormControl (ngModelChange)="calcline($event)"
id="floatingInput" placeholder="Snippet code" [(ngModel)]="current_snippet['content']"></textarea>
<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>
</c-input-group>
<br />
@ -230,27 +307,28 @@
</c-modal-footer>
</c-modal>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible"
id="DeleteConfirmModal">
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedSnippet['name'] }}</h5>
<h5 cModalTitle>Confirm delete {{ SelectedSnippet['name'] }}</h5>
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
Are you sure that You want to delete following Snippet ?
<br/>
<br/>
<br />
<br />
<table style="width: 100%;">
<tr>
<td><b>Snippet name : </b>{{ SelectedSnippet['name'] }}
</tr>
<tr>
</tr>
<tr>
<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>
</td>
</tr>
<tr>
<td><b>Snippet name : </b>{{ SelectedSnippet['name'] }}
</tr>
<tr>
</tr>
<tr>
<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>
</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
@ -263,6 +341,4 @@
</c-modal-footer>
</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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import { HttpClient, HttpClientModule, 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 { HttpClient, HttpParams } from "@angular/common/http";
import { formatInTimeZone } from "date-fns-tz";
import { Table } from 'primeng/table';
@Component({
templateUrl: "snippets.component.html",
})
export class SnippetsComponent implements OnInit, OnDestroy {
public uid: number;
public uname: string;
public tz: string;
public uid!: number;
public uname!: string;
public tz!: string;
public ispro: boolean = false;
constructor(
private data_provider: dataProvider,
@ -56,7 +43,8 @@ export class SnippetsComponent implements OnInit, OnDestroy {
// console.dir("res",res)
_self.uid = res.uid;
_self.uname = res.name;
_self.tz = res.tz;
_self.tz = res.tz;
_self.ispro = res.ispro;
// console.dir("role",res.role);
const userId = _self.uid;
@ -73,9 +61,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
}
}
@ViewChild("nameSummaryCell")
nameSummaryCell: TemplateRef<any>;
nameSummaryCell!: TemplateRef<any>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public columns: Array<any> = [];
public loading: boolean = true;
public rows: any = [];
public Selectedrows: any;
@ -94,6 +82,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
public NewMemberRows: any = [];
public SelectedNewMemberRows: any;
@ViewChild('dtNewMember') dtNewMember!: Table;
@ViewChild('dtHistory') dtHistory!: Table;
public current_snippet: any = {
content: "",
created: "",
@ -110,44 +101,15 @@ export class SnippetsComponent implements OnInit, OnDestroy {
name: "",
};
public sorting = {
enabled: true,
multiSorting: true,
};
public ip_scanner: any;
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
applyFilterNewMember($event: any, stringVal: string) {
this.dtNewMember.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
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,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
applyFilterHistory($event: any, stringVal: string) {
this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
ngOnInit(): void {
this.initGridTable();
@ -182,31 +144,31 @@ export class SnippetsComponent implements OnInit, OnDestroy {
this.ModalAction = "edit";
}
}
show_exec(item:any){
var _self=this;
this.SelectedSnippet = item;
this.ExecutedDataModalVisible = true;
this.data_provider
.get_executed_snipet(_self.SelectedSnippet["id"])
.then((res) => {
let index = 1;
_self.ExecutedData= res.map((d: any) => {
d.index = index;
d.ended = formatInTimeZone(
d.created.split(".")[0] + ".000Z",
_self.tz,
"yyyy-MM-dd HH:mm:ss XXX"
);
d.started = formatInTimeZone(
d.info.created.split(".")[0] + ".000Z",
_self.tz,
"yyyy-MM-dd HH:mm:ss XXX"
);
index += 1;
return d;
});
_self.DeleteConfirmModalVisible = false;
});
show_exec(item: any) {
var _self = this;
this.SelectedSnippet = item;
this.ExecutedDataModalVisible = true;
this.data_provider
.get_executed_snipet(_self.SelectedSnippet["id"])
.then((res) => {
let index = 1;
_self.ExecutedData = res.map((d: any) => {
d.index = index;
d.ended = formatInTimeZone(
d.created.split(".")[0] + ".000Z",
_self.tz,
"yyyy-MM-dd HH:mm:ss XXX"
);
d.started = formatInTimeZone(
d.info.created.split(".")[0] + ".000Z",
_self.tz,
"yyyy-MM-dd HH:mm:ss XXX"
);
index += 1;
return d;
});
_self.DeleteConfirmModalVisible = false;
});
}
form_changed() {
@ -254,11 +216,11 @@ export class SnippetsComponent implements OnInit, OnDestroy {
});
}
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
onSelectedRowsNewMembers(rows: Array<any>): void {
this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
this.SelectedNewMemberRows = rows.map((m: any) => m.source || m);
}
add_new_members() {
var _self = this;
_self.SelectedMembers = [
@ -274,12 +236,12 @@ export class SnippetsComponent implements OnInit, OnDestroy {
submit(action: string) {
var _self = this;
this.data_provider
.Exec_snipet(_self.current_snippet, _self.SelectedTaskItems)
.then((res) => {
_self.initGridTable();
});
this.data_provider
.Exec_snipet(_self.current_snippet, _self.SelectedTaskItems)
.then((res) => {
_self.initGridTable();
});
this.ExecSnipetModalVisible = false;
}
@ -287,7 +249,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
this.current_snippet = item;
this.current_snippet["task_type"] = "snipet_exec";
this.current_snippet["selection_type"] = "devices";
this.form_changed();
this.form_changed();
this.ExecSnipetModalVisible = true;
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.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
this.Selectedrows = rows.map((m: any) => (m.source ? m.source.id : m.id));
}
remove(item: any) {
@ -318,7 +280,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
initGridTable(): void {
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) => {
x.created = [
x.created.split("T")[0],
@ -330,18 +292,18 @@ export class SnippetsComponent implements OnInit, OnDestroy {
});
}
sanitizeString(desc:string) {
var itemDesc:string='';
sanitizeString(desc: string) {
var itemDesc: string = '';
if (desc) {
itemDesc = desc.toString().replace(/"/g, '\"');
itemDesc = itemDesc.replace(/'/g, '\'');
itemDesc = desc.toString().replace(/"/g, '\"');
itemDesc = itemDesc.replace(/'/g, '\'');
} else {
itemDesc = '';
itemDesc = '';
}
return itemDesc;
}
exportToCsv(jsonResponse:any) {
exportToCsv(jsonResponse: any) {
const data = jsonResponse;
const columns = this.getColumns(data);
const csvData = this.convertToCsv(data, columns);
@ -349,7 +311,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
}
getColumns(data: any[]): string[] {
const columns : any = [];
const columns: any = [];
data.forEach(row => {
Object.keys(row).forEach((col) => {
if (!columns.includes(col)) {
@ -361,13 +323,13 @@ export class SnippetsComponent implements OnInit, OnDestroy {
}
convertToCsv(data: any[], columns: string[]): string {
var _self=this;
var _self = this;
let csv = '';
csv += columns.join(',') + '\n';
data.forEach(row => {
const values : any = [];
columns.forEach((col:any) => {
values.push('"'+_self.sanitizeString(row[col])+'"');
const values: any = [];
columns.forEach((col: any) => {
values.push('"' + _self.sanitizeString(row[col]) + '"');
});
csv += values.join(',') + '\n';
});
@ -376,10 +338,10 @@ export class SnippetsComponent implements OnInit, OnDestroy {
downloadFile(data: string, filename: string, type: string) {
const blob = new Blob([data], { type: type });
const nav = (window.navigator as any);
const nav = (window.navigator as any);
if (nav.msSaveOrOpenBlob) {
nav.msSaveBlob(blob, filename);
nav.msSaveBlob(blob, filename);
} else {
const link = document.createElement('a');
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";
import { SnippetsRoutingModule } from "./snippets-routing.module";
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({
imports: [
@ -24,7 +26,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule,
ToastModule,
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: '',
component: SyslogComponent,
data: {
title: $localize`Mikrowizard System Logs`
title: $localize`System Logs`
}
}
];

View file

@ -4,9 +4,11 @@
<c-card-header>
<c-row>
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
<h5>Devices</h5>
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *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.
<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']">
<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-col>
<c-col xs [lg]="1">
@ -62,44 +64,144 @@
</div>
</c-row>
<gui-grid wid [rowDetail]="rowDetail" [horizontalGrid]="true" [rowHeight]="52" [source]="source"
[columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting" [infoPanel]="infoPanel"
[autoResizeWidth]="true">
<gui-grid-column header="#No" type="NUMBER" field="index" width="1" align="CENTER">
<ng-template let-value="item.index" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
<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 logs..." (input)="applyFilterGlobal($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<gui-grid-column header="username" field="username">
<ng-template let-value="item.username" 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.first_name }} {{ item.last_name }}</span>
<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]="['username','section','action','ip']" selectionMode="single"
(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>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Section" field="section">
<ng-template let-value="item.section" let-item="item" let-index="index">
{{ value }}
</ng-template>
</gui-grid-column>
<gui-grid-column header="action" field="action">
<ng-template let-value="item.action" let-item="item" let-index="index">
{{ 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>
</div>
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-code me-2 text-primary"></i>Raw Data</h5>
<div class="bg-light p-3 rounded"
style="max-height: 250px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85rem; border: 1px solid #dee2e6;">
{{selectedLog.data}}
</div>
</div>
</p-drawer>
</c-card-body>
</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 { Router, ActivatedRoute } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiRowDetail,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { formatInTimeZone } from "date-fns-tz";
@ -22,10 +12,11 @@ import { formatInTimeZone } from "date-fns-tz";
encapsulation: ViewEncapsulation.None,
})
export class SyslogComponent implements OnInit {
public uid: number;
public uname: string;
public uid!: number;
public uname!: string;
public tz: string= "UTC";
public filterText: string;
public filterText!: string;
public userid: number = 0;
public filters: any = {
start_time: false,
end_time: false,
@ -36,6 +27,10 @@ export class SyslogComponent implements OnInit {
public event_section: any = [];
public event_action: any = [];
public filters_visible: boolean = false;
public detailsVisible: boolean = false;
public selectedLog: any = null;
@ViewChild('dt') table!: Table;
constructor(
private data_provider: dataProvider,
private router: Router,
@ -66,95 +61,18 @@ export class SyslogComponent implements OnInit {
}
}
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public Selectedrows: any;
public userid: number = 0;
public sorting = {
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>`;
},
};
showLogDetails(log: any) {
this.selectedLog = log;
this.detailsVisible = true;
}
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
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,
};
public infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true,
};
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
// Removed legacy GuiGrid configs
ngOnInit(): void {
var _self = this;
this.userid = Number(this.route.snapshot.paramMap.get("userid"));

View file

@ -12,7 +12,9 @@ import {
import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
import { SyslogRoutingModule } from "./syslog-routing.module";
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 { MatInputModule } from "@angular/material/input";
import { MatFormFieldModule } from "@angular/material/form-field";
@ -28,7 +30,9 @@ import { MatSelectModule } from "@angular/material/select";
GridModule,
FormsModule,
ButtonModule,
GuiGridModule,
TableModule,
DrawerModule,
InputTextModule,
CollapseModule,
DropdownModule,
MatInputModule,

View file

@ -4,7 +4,7 @@
<c-card-header>
<c-row>
<c-col xs [lg]="10">
Users
User Managment
</c-col>
<c-col xs [lg]="2" style="text-align: right;">
<button cButton color="primary" (click)="editAddUser({},'showadd')"><i
@ -12,41 +12,91 @@
</c-col>
</c-row>
</c-card-header>
<c-card-body >
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting"
[autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="User Name" field="username">
<ng-template let-value="item.username" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column header="First Name" field="first_name">
<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>
<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 users..." (input)="applyFilterGlobal($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<button *ngIf="ispro" cButton color="secondary" size="sm" (click)="showrest(item);">
<i class="fa-solid fa-fingerprint"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<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]="['username', 'first_name', 'last_name', 'role']" [loading]="loading">
<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>
</c-col>
@ -54,285 +104,304 @@
<c-modal-header>
<c-modal #EditTaskModal backdrop="static" size="lg" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<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']=='add'" cModalTitle><i class="fa-solid fa-user-plus me-1"></i>Add User</h5>
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-3">
<!-- User Details -->
<div class="user-form-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-user me-2"></i>Basic Information</h6>
<small class="text-muted">Enter the user's personal details and login credentials</small>
</div>
<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>
<c-modal #EditTaskModal backdrop="static" size="lg" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<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']=='add'" cModalTitle><i class="fa-solid fa-user-plus me-1"></i>Add User</h5>
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-3">
<!-- User Details -->
<div class="user-form-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-user me-2"></i>Basic Information</h6>
<small class="text-muted">Enter the user's personal details and login credentials</small>
</div>
</div>
</div>
<!-- Device Permissions -->
<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-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="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 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="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 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>
<!-- 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>
<!-- 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 *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 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>
<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>
<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" />
<!-- Device Permissions -->
<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>
</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>
<!-- 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 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>
<c-modal #DeletePermConfirmModal backdrop="static" [(visible)]="DeletePermConfirmModalVisible" id="DeletePermConfirmModal">
<c-modal #DeletePermConfirmModal backdrop="static" [(visible)]="DeletePermConfirmModalVisible"
id="DeletePermConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedUser['name'] }}</h5>
<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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiGridComponent,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { NgxSuperSelectOptions } from "ngx-super-select";
import { AppToastComponent } from "../toast-simple/toast.component";
import { ToasterComponent } from "@coreui/angular";
@ -21,11 +12,11 @@ import { ToasterComponent } from "@coreui/angular";
styleUrls: ["user_manager.scss"],
})
export class UserManagerComponent implements OnInit {
public uid: number;
public uname: string;
public ispro:boolean=false;
public uid: number = 0;
public uname: string = '';
public ispro: boolean = false;
gridComponent: GuiGridComponent;
@ViewChild('dt') table!: Table;
toasterForm = {
autohide: true,
delay: 10000,
@ -64,7 +55,6 @@ export class UserManagerComponent implements OnInit {
}
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = false;
public rows: any = [];
public SelectedUser: any = {};
@ -87,8 +77,8 @@ export class UserManagerComponent implements OnInit {
public DeletePermConfirmModalVisible: boolean = false;
public userperms: any = {};
public userresttrictions: any = false;
public ipaddress:string="";
public adminperms: { [index: string]: string };
public ipaddress: string = "";
public adminperms: { [index: string]: string } = {};
public defadminperms: { [index: string]: string } = {
device: "none",
device_group: "none",
@ -103,11 +93,6 @@ export class UserManagerComponent implements OnInit {
system_backup: "none",
};
public sorting = {
enabled: true,
multiSorting: true,
};
options: Partial<NgxSuperSelectOptions> = {
actionsEnabled: false,
displayExpr: "name",
@ -117,34 +102,20 @@ export class UserManagerComponent implements OnInit {
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 {
this.adminperms[key] = value;
}
filterDevGroups(event: any): void {
const query = event.target.value.toLowerCase();
this.filteredDevGroups = this.allDevGroups.filter((group: any) =>
this.filteredDevGroups = this.allDevGroups.filter((group: any) =>
group.name.toLowerCase().includes(query)
);
}
filterPermissions(event: any): void {
const query = event.target.value.toLowerCase();
this.filteredPermissions = this.allPerms.filter((perm: any) =>
this.filteredPermissions = this.allPerms.filter((perm: any) =>
perm.name.toLowerCase().includes(query)
);
}
@ -169,11 +140,9 @@ export class UserManagerComponent implements OnInit {
setTimeout(() => this.showPermissionDropdown = false, 200);
}
public rowSelection: boolean | GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE,
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
ngOnInit(): void {
this.initGridTable();
@ -188,12 +157,12 @@ export class UserManagerComponent implements OnInit {
);
componentRef.instance["closeButton"] = props.closeButton;
}
totp(item:any){
totp(item: any) {
this.SelectedUser = item;
this.data_provider.totp('enable',this.SelectedUser.id).then((res) => {
if(res.status == "success"){
this.data_provider.totp('enable', this.SelectedUser.id).then((res) => {
if (res.status == "success") {
this.show_toast("Success", "Totp generated successfully", "success");
}else{
} else {
this.show_toast("Error", res.err, "danger");
}
});
@ -218,15 +187,15 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
if ("id" in res && !("status" in res)) {
_self.initGridTable();
this.EditTaskModalVisible = false;
} else {
//show error
_self.show_toast("Error", res.err, "danger");
else {
if ("id" in res && !("status" in res)) {
_self.initGridTable();
this.EditTaskModalVisible = false;
} else {
//show error
_self.show_toast("Error", res.err, "danger");
}
}
}
});
} else {
if (_self.userperms.length > 0) {
@ -243,9 +212,9 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
_self.initGridTable();
_self.EditTaskModalVisible = false;
else {
_self.initGridTable();
_self.EditTaskModalVisible = false;
}
});
}
@ -262,27 +231,30 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
_self.allPerms = res.map((x: any) => {
return { id: x["id"], name: x.name };
});
_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) => {
else if ("err" in res) {
_self.show_toast("Error", res.err, "danger");
}
else {
_self.allPerms = res.map((x: any) => {
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") {
this.userperms = [];
@ -303,6 +275,10 @@ export class UserManagerComponent implements OnInit {
this.EditTaskModalVisible = true;
return;
}
if (item.username == "system") {
this.show_toast("Error", "System user cannot be edited", "danger");
return;
}
this.SelectedUser = { ...item };
if (this.SelectedUser["adminperms"].length > 0) {
this.adminperms = JSON.parse(this.SelectedUser["adminperms"]);
@ -316,13 +292,13 @@ export class UserManagerComponent implements OnInit {
_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$/;
return ipv4Pattern.test(ip)
}
showrest(item: any) {
var _self=this;
var _self = this;
this.SelectedUser = { ...item };
this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => {
@ -330,33 +306,33 @@ export class UserManagerComponent implements OnInit {
_self.RestrictionsTaskModalVisible = true;
});
}
delete_ip(item:string){
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!=item);
delete_ip(item: string) {
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
let ip=this.ipaddress.trim();
if(ip=="")return;
if(this.userresttrictions['allowed_ips'].includes(ip)){
let ip = this.ipaddress.trim();
if (ip == "") return;
if (this.userresttrictions['allowed_ips'].includes(ip)) {
this.show_toast("Error", "IP already added", "danger");
return;
}
//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']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!="");
this.ipaddress="";
this.userresttrictions['allowed_ips'] = this.userresttrictions['allowed_ips'].filter((x: any) => x != "");
this.ipaddress = "";
}
else{
else {
this.show_toast("Error", "Invalid IP address", "danger");
}
}
save_sec(){
var _self=this;
this.data_provider.save_user_restrictions(this.SelectedUser.id,this.userresttrictions).then((res) => {
save_sec() {
var _self = this;
this.data_provider.save_user_restrictions(this.SelectedUser.id, this.userresttrictions).then((res) => {
if ("error" in res && res.error.indexOf("Unauthorized")) {
_self.show_toast(
"Error",
@ -364,17 +340,17 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
if('status' in res && res['status']=='success')
else {
if ('status' in res && res['status'] == 'success')
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");
else
else
this.show_toast("Error", "Somthing went wrong", "danger");
}
});
}
add_user_perm() {
var _self = this;
this.data_provider
@ -391,10 +367,10 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
_self.get_user_perms(_self.SelectedUser["id"]);
_self.permission = 0;
_self.devgroup = 0;
else {
_self.get_user_perms(_self.SelectedUser["id"]);
_self.permission = 0;
_self.devgroup = 0;
}
});
}
@ -425,9 +401,12 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
_self.initGridTable();
_self.DeleteConfirmModalVisible = false;
else if ("err" in res) {
_self.show_toast("Error", res.err, "danger");
}
else {
_self.initGridTable();
_self.DeleteConfirmModalVisible = false;
}
});
}
@ -451,12 +430,12 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
this.get_user_perms(this.SelectedUser["id"]);
else {
this.get_user_perms(this.SelectedUser["id"]);
}
});
}
logger(item: any) {
console.dir(item);
}
@ -474,13 +453,13 @@ export class UserManagerComponent implements OnInit {
"danger"
);
}
else{
_self.source = res.map((x: any) => {
return x;
});
_self.SelectedUser = {};
_self.loading = false;
}
else {
_self.source = res.map((x: any) => {
return x;
});
_self.SelectedUser = {};
_self.loading = false;
}
});
}
}

View file

@ -16,7 +16,9 @@ import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
import { UserManagerRoutingModule } from "./user_manager-routing.module";
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({
imports: [
@ -29,7 +31,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
TableModule,
InputTextModule,
TooltipModule,
ModalModule,
FormsModule,
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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
import { AppToastComponent } from "../toast-simple/toast.component";
@ -25,10 +14,13 @@ import { ToasterComponent } from "@coreui/angular";
styleUrls: ["user_tasks.component.scss"]
})
export class UserTasksComponent implements OnInit {
public uid: number;
public uname: string;
public uid: number = 0;
public uname: string = '';
public ispro: boolean = false;
@ViewChild('dt') table!: Table;
@ViewChild('dtNewMembers') tableNewMembers!: Table;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
toasterForm = {
autohide: true,
@ -67,7 +59,6 @@ export class UserTasksComponent implements OnInit {
}
}
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public SelectedTask: any = {};
@ -77,6 +68,7 @@ export class UserTasksComponent implements OnInit {
public DeleteConfirmModalVisible: boolean = false;
public Members: any = "";
public Snippets: any;
public Sequences: any = [];
public SelectedMembers: any = [];
public NewMemberModalVisible: boolean = false;
public availbleMembers: any = [];
@ -96,7 +88,7 @@ export class UserTasksComponent implements OnInit {
{ label: 'Every 10 minutes', value: '*/10 * * * *', description: 'Regular monitoring checks' },
{ label: 'Every 15 minutes', value: '*/15 * * * *', description: 'Moderate monitoring frequency' },
{ label: 'Every 30 minutes', value: '*/30 * * * *', description: 'Low frequency monitoring' },
// Hourly Operations
{ label: 'Every hour', value: '0 * * * *', description: 'Hourly network checks' },
{ label: 'Every 2 hours', value: '0 */2 * * *', description: 'Bi-hourly operations' },
@ -104,7 +96,7 @@ export class UserTasksComponent implements OnInit {
{ label: 'Every 6 hours', value: '0 */6 * * *', description: 'Four times daily' },
{ label: 'Every 8 hours', value: '0 */8 * * *', description: 'Three times daily' },
{ label: 'Every 12 hours', value: '0 */12 * * *', description: 'Twice daily operations' },
// Daily Maintenance
{ label: 'Daily at midnight', value: '0 0 * * *', description: 'Daily maintenance at 00:00' },
{ label: 'Daily at 1 AM', value: '0 1 * * *', description: 'Daily backup at 01:00' },
@ -113,20 +105,20 @@ export class UserTasksComponent implements OnInit {
{ label: 'Daily at 6 AM', value: '0 6 * * *', description: 'Pre-business hours check' },
{ label: 'Daily at 6 PM', value: '0 18 * * *', description: 'End of business day backup' },
{ label: 'Daily at 10 PM', value: '0 22 * * *', description: 'Evening maintenance at 22:00' },
// Business Hours
{ label: 'Workdays at 8 AM', value: '0 8 * * 1-5', description: 'Start of business day - Mon to Fri' },
{ label: 'Workdays at 9 AM', value: '0 9 * * 1-5', description: 'Business hours start check' },
{ label: 'Workdays at 12 PM', value: '0 12 * * 1-5', description: 'Midday check - Mon to Fri' },
{ label: 'Workdays at 5 PM', value: '0 17 * * 1-5', description: 'End of business day - Mon to Fri' },
{ label: 'Workdays at 6 PM', value: '0 18 * * 1-5', description: 'After hours backup - Mon to Fri' },
// Weekly Operations
{ label: 'Weekly (Sunday midnight)', value: '0 0 * * 0', description: 'Weekly maintenance - Sunday 00:00' },
{ label: 'Weekly (Monday midnight)', value: '0 0 * * 1', description: 'Weekly start - Monday 00:00' },
{ label: 'Weekly (Friday 6 PM)', value: '0 18 * * 5', description: 'End of week backup - Friday 18:00' },
{ label: 'Weekly (Saturday 2 AM)', value: '0 2 * * 6', description: 'Weekend maintenance - Saturday 02:00' },
// Monthly Operations
{ label: 'Monthly (1st at midnight)', value: '0 0 1 * *', description: 'Monthly maintenance - 1st of month' },
{ label: 'Monthly (1st at 2 AM)', value: '0 2 1 * *', description: 'Monthly backup - 1st at 02:00' },
@ -137,14 +129,13 @@ export class UserTasksComponent implements OnInit {
public cronSearch: string = '';
public selectedCronPreset: any = null;
public filteredCrons: any[] = [];
public sorting = {
enabled: true,
multiSorting: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
applyFilterGlobal($event: any, stringVal: string) {
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
applyFilterNewMembers($event: any, stringVal: string) {
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
options: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
@ -156,32 +147,16 @@ export class UserTasksComponent implements OnInit {
enableDarkMode: false,
};
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
seqOptions: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
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() {
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.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
this.SelectedNewMemberRows = rows;
}
add_new_members() {
@ -299,15 +274,17 @@ export class UserTasksComponent implements OnInit {
var _self = this;
this.SelectedTask = { ...item };
// Initialize cron search and preset tracking
this.cronSearch = '';
const currentCron = this.SelectedTask['cron'];
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) {
this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']);
if (this.SelectedTask['data']['strategy'] == 'defined') {
if ((this.SelectedTask['task_type'] == 'firmware' || this.SelectedTask['task_type'] == 'sequence') && 'data' in this.SelectedTask && this.SelectedTask['data']) {
if (typeof this.SelectedTask['data'] === 'string') {
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) => {
let index = 1;
_self.available_firmwares = [
@ -328,16 +305,23 @@ export class UserTasksComponent implements OnInit {
_self.firms_loaded = true;
});
}
else{
else {
_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) => {
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") {
this.SelectedTask["action"] = "edit";
this.data_provider.get_task_members(_self.SelectedTask.id).then((res) => {
@ -354,7 +338,7 @@ export class UserTasksComponent implements OnInit {
}
firmware_type_changed(type: any) {
this.SelectedTask['data']['strategy'] = type;
if (type == 'system') {
@ -405,7 +389,7 @@ export class UserTasksComponent implements OnInit {
onSnippetsValueChanged(v: any) {
var _self = this;
if (v == "" || v.length < 3) return;
_self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => {
_self.data_provider.get_snippets(v, "", "", 0, 1000, false).then((res) => {
_self.Snippets = res.map((x: any) => {
return { id: String(x.id), name: x.name };
});
@ -470,10 +454,10 @@ export class UserTasksComponent implements OnInit {
const searchTerm = event.target.value.toLowerCase();
this.cronSearch = searchTerm;
this.selectedCronPreset = null;
if (searchTerm.length > 0) {
this.filteredCrons = this.predefinedCrons.filter(cron =>
cron.label.toLowerCase().includes(searchTerm) ||
this.filteredCrons = this.predefinedCrons.filter(cron =>
cron.label.toLowerCase().includes(searchTerm) ||
cron.description.toLowerCase().includes(searchTerm) ||
cron.value.includes(searchTerm)
);
@ -491,7 +475,7 @@ export class UserTasksComponent implements OnInit {
onCronInputFocus(): void {
this.filteredCrons = this.predefinedCrons;
this.showCronDropdown = true;
// If current cron matches a preset, highlight it
const currentCron = this.SelectedTask['cron'];
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
@ -500,7 +484,7 @@ export class UserTasksComponent implements OnInit {
onCronInputChange(event: any): void {
this.SelectedTask['cron'] = event.target.value;
this.selectedCronPreset = null;
// Check if the entered value matches any preset
const enteredValue = event.target.value;
const matchingPreset = this.predefinedCrons.find(cron => cron.value === enteredValue);
@ -513,7 +497,7 @@ export class UserTasksComponent implements OnInit {
if (this.selectedCronPreset) {
return this.selectedCronPreset.description;
}
const currentCron = this.SelectedTask['cron'];
const matchingPreset = this.predefinedCrons.find(cron => cron.value === currentCron);
return matchingPreset ? matchingPreset.description : 'Custom cron expression';
@ -522,6 +506,8 @@ export class UserTasksComponent implements OnInit {
onTaskTypeChange(): void {
if (this.SelectedTask['task_type'] === 'snippet') {
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";
import { UserTasksRoutingModule } from "./user_tasks-routing.module";
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";
@ -31,7 +33,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
BadgeModule,
AlertModule,
ToastModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule,
ReactiveFormsModule,
FormsModule,

View file

@ -1,22 +1,26 @@
<div style="position: relative;">
<c-card class="mb-4" style="margin-bottom: 0 !important;">
<c-card-header style="padding: 0;">
<c-row class="align-items-center">
<c-col xs="12" md="6">
<div class="nav nav-underline bg-transparent">
<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
</a>
</div>
<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
</a>
</div>
</div>
</c-col>
<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
</button>
<button *ngIf="activetab==1" cButton size="sm" (click)="toggleCollapse()" color="info">
@ -36,11 +40,12 @@
<i class="fas fa-cog text-primary me-2 fs-4"></i>
<div>
<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>
<!-- Password Vault Configuration -->
<div class="mb-4 pb-4 border-bottom">
<h6 class="mb-3"><i class="fas fa-shield-alt me-2"></i>Password Vault Configuration</h6>
@ -54,7 +59,8 @@
</select>
</c-col>
<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">
<option>Choose...</option>
<option value="all">All Local Users</option>
@ -62,7 +68,8 @@
</select>
</c-col>
<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">
<option>Choose...</option>
<option value="daily">Daily</option>
@ -74,7 +81,8 @@
</select>
</c-col>
<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">
<option>Choose...</option>
<option value="random">Random</option>
@ -104,20 +112,26 @@
</c-col>
</c-row>
<div *ngIf="settings['exceptions']?.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="settings['exceptions']" [columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting">
<gui-grid-column header="Username" field="name">
<ng-template let-value="item" let-item="item" let-index="index">
<i class="fas fa-user me-2 text-muted"></i>{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="80" field="action" align="center">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button (click)="remove_exception(item)" cButton color="danger" size="sm" variant="outline">
<i class="fas fa-trash"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<p-table [value]="settings['exceptions']" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name">Username <p-sortIcon field="name"></p-sortIcon></th>
<th style="width: 80px" class="text-center">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><i class="fas fa-user me-2 text-muted"></i>{{item}}</td>
<td class="text-center">
<button (click)="remove_exception(item)" cButton color="danger" size="sm" variant="outline"
pTooltip="Remove Exception">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
<!-- Pre-defined Passwords -->
@ -137,20 +151,26 @@
</c-col>
</c-row>
<div *ngIf="settings['passwords']?.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="settings['passwords']" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging">
<gui-grid-column header="Password" field="name">
<ng-template let-value="item" let-item="item" let-index="index">
<i class="fas fa-lock me-2 text-muted"></i>••••••••
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="80" field="action" align="center">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="danger" size="sm" variant="outline" (click)="remove_password(item)">
<i class="fas fa-trash"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<p-table [value]="settings['passwords']" [showGridlines]="true" [stripedRows]="true"
styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name">Password <p-sortIcon field="name"></p-sortIcon></th>
<th style="width: 80px" class="text-center">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><i class="fas fa-lock me-2 text-muted"></i>••••••••</td>
<td class="text-center">
<button cButton color="danger" size="sm" variant="outline" (click)="remove_password(item)"
pTooltip="Remove Password">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
<!-- Affected Groups -->
@ -167,20 +187,25 @@
</c-col>
</c-row>
<div *ngIf="Members?.length > 0" class="mb-3">
<gui-grid [autoResizeWidth]="true" [source]="Members" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging">
<gui-grid-column header="Group Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<i class="fas fa-server me-2 text-primary"></i>{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="80" field="action" align="center">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button (click)="delete_group(item.id)" cButton color="danger" size="sm" variant="outline">
<i class="fas fa-trash"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<p-table [value]="Members" [showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name">Group Name <p-sortIcon field="name"></p-sortIcon></th>
<th style="width: 80px" class="text-center">Actions</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><i class="fas fa-server me-2 text-primary"></i>{{item.name}}</td>
<td class="text-center">
<button (click)="delete_group(item.id)" cButton color="danger" size="sm" variant="outline"
pTooltip="Remove Group">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div>
<div class="text-end">
<button cButton color="success" (click)="save_settings()">
@ -192,25 +217,28 @@
<!-- Execution History -->
<div *ngIf="vault_history">
<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">
<gui-grid-column header="Start Time" field="started">
<ng-template let-value="item.started" let-item="item" let-index="index">
<small>{{value}}</small>
</ng-template>
</gui-grid-column>
<gui-grid-column header="End Time" field="ended">
<ng-template let-value="item.ended" let-item="item" let-index="index">
<small>{{value}}</small>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Results" field="result" align="center" width="100">
<ng-template let-value="item['result']" let-item="item" let-index="index">
<button (click)="exportToCsv(value)" color="primary" size="sm" cButton variant="outline">
<i class="fas fa-download"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<p-table #dtHistory [value]="vault_history" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" styleClass="p-datatable-sm">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="started">Start Time <p-sortIcon field="started"></p-sortIcon></th>
<th pSortableColumn="ended">End Time <p-sortIcon field="ended"></p-sortIcon></th>
<th style="width: 100px" class="text-center">Results</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-item>
<tr>
<td><small>{{item.started}}</small></td>
<td><small>{{item.ended}}</small></td>
<td class="text-center">
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton variant="outline"
pTooltip="Download CSV">
<i class="fas fa-download"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div>
</c-card-body>
</c-card>
@ -224,11 +252,12 @@
<i class="fas fa-key text-warning me-2 fs-4"></i>
<div>
<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>
<!-- Filters -->
<div class="mb-4 pb-4 border-bottom" [visible]="filters_visible" cCollapse>
<h6 class="mb-3"><i class="fas fa-filter me-2"></i>Filters</h6>
@ -257,41 +286,59 @@
<!-- Stored Passwords -->
<div *ngIf="passwords">
<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">
<gui-grid-column header="Device" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<div class="d-flex align-items-center">
<i class="fas fa-router me-2 text-primary"></i>
<div>
<div class="fw-semibold">{{value}}</div>
<small class="text-muted">{{item.devip}}</small>
<p-table #dtPasswords [value]="passwords" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
[globalFilterFields]="['name', 'devip', 'username', 'changed']" [loading]="loading">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="name" pResizableColumn>
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>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Username" field="username">
<ng-template let-value="item.username" let-item="item" let-index="index">
<i class="fas fa-user me-2 text-muted"></i>{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Last Changed" field="changed">
<ng-template let-value="item.changed" let-item="item" let-index="index">
<small>{{value}}</small>
</ng-template>
</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>
</td>
<td><i class="fas fa-user me-2 text-muted"></i>{{item.username}}</td>
<td><small>{{item.changed}}</small></td>
<td class="text-center">
<button cButton *ngIf="ispro" (click)="reveal_password(item.devid,item.username)" color="info"
size="sm" variant="outline" pTooltip="View Password">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
</ng-template>
</p-table>
</div>
</c-card-body>
</c-card>
</c-tab-pane>
</c-tab-content>
<app-license-expired-overlay></app-license-expired-overlay>
</div>
<!-- Password Reveal Modal -->
<c-modal #PasswordModal backdrop="static" [(visible)]="PasswordModalVisible" id="PasswordModal">
@ -306,7 +353,7 @@
</div>
<div class="input-group">
<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)">
<i class="fas fa-copy"></i>
</button>
@ -325,7 +372,8 @@
</c-modal-header>
<c-modal-body>
<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>
<p>Are you sure you want to proceed with the password vault execution?</p>
</c-modal-body>
@ -345,25 +393,50 @@
</c-modal-header>
<c-modal-body>
<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"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging">
<gui-grid-column header="Group Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<i class="fas fa-server me-2 text-primary"></i>{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" 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 *ngIf="SelectedTask['selection_type']=='devices'" 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>
<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 groups..." (input)="applyFilterNewMembers($event, 'contains')"
class="form-control-sm" />
</span>
</div>
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped" [(selection)]="NewMemberRows"
(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>
Name
<p-sortIcon field="name"></p-sortIcon>
<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-footer>
<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 { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
import {
GuiSelectedRow,
GuiSearching,
GuiInfoPanel,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiRowSelectionMode,
GuiRowSelection,
GuiRowSelectionType,
} from "@generic-ui/ngx-grid";
import { Table } from 'primeng/table';
import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
import { formatInTimeZone } from "date-fns-tz";
@ -27,10 +16,14 @@ import { AppToastComponent } from "../toast-simple/toast.component";
})
export class VaultComponent implements OnInit {
public uid: number;
public uname: string;
public uid: number = 0;
public uname: string = '';
public ispro: boolean = false;
public tz: string;
public tz: string = '';
@ViewChild('dtPasswords') dtPasswords!: Table;
@ViewChild('dtHistory') dtHistory!: Table;
@ViewChild('dtNewMembers') dtNewMembers!: Table;
constructor(
private data_provider: dataProvider,
@ -71,7 +64,6 @@ export class VaultComponent implements OnInit {
public password:string="";
public PasswordModalVisible:boolean=false;
public source: Array<any> = [];
public columns: Array<GuiColumn> = [];
public loading: boolean = true;
public rows: any = [];
public SelectedTask: any = {};
@ -87,16 +79,7 @@ export class VaultComponent implements OnInit {
public filters: any = {};
public activetab:number=0;
public sorting = {
enabled: true,
multiSorting: true,
};
searching: GuiSearching = {
enabled: true,
placeholder: "Search Devices",
};
toasterForm = {
public toasterForm = {
autohide: true,
delay: 3000,
position: "fixed",
@ -104,42 +87,17 @@ export class VaultComponent implements OnInit {
closeButton: true,
};
options: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Snippet",
searchEnabled: true,
enableDarkMode: false,
};
applyFilterPasswords($event: any, stringVal: string) {
this.dtPasswords.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
public paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 10,
pageSizes: [5, 10, 25, 50],
display: GuiPagingDisplay.ADVANCED,
};
applyFilterHistory($event: any, stringVal: string) {
this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
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,
};
applyFilterNewMembers($event: any, stringVal: string) {
this.dtNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
}
reinitgrid(field: string, $event: any) {
if (field == "username") this.filters["username"] = $event;
@ -153,10 +111,9 @@ export class VaultComponent implements OnInit {
this.get_vault_history();
}
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
onSelectedRowsNewMembers(rows: any[]): void {
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 {

View file

@ -16,10 +16,13 @@ import {
} from "@coreui/angular";
import { VaultRoutingModule } from "./vault-routing.module";
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 { MatFormFieldModule } from "@angular/material/form-field";
import { SharedModule } from "../../shared/shared.module";
@NgModule({
imports: [
VaultRoutingModule,
@ -29,7 +32,9 @@ import { MatFormFieldModule } from "@angular/material/form-field";
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
PTableModule,
PInputTextModule,
PTooltipModule,
ModalModule,
ReactiveFormsModule,
FormsModule,
@ -38,7 +43,8 @@ import { MatFormFieldModule } from "@angular/material/form-field";
MatInputModule,
MatFormFieldModule,
CollapseModule,
TooltipModule
TooltipModule,
SharedModule
],
declarations: [VaultComponent],
})

View file

@ -63,12 +63,6 @@
}
}
// Grid Improvements
gui-grid {
.gui-grid {
font-size: 0.85rem;
}
}
// Input Group Compact
.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-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
ngx-super-select.styled>div > span{
background:var(--cui-input-group-addon-bg, #d8dbe0);
height:100%;
padding:8px;
color:var(--cui-input-group-addon-color, rgba(44, 56, 74, 0.95))!important;
ngx-super-select.styled>div>span {
background: var(--cui-input-group-addon-bg, #d8dbe0);
height: 100%;
padding: 8px;
color: var(--cui-input-group-addon-color, rgba(44, 56, 74, 0.95)) !important;
}
ngx-super-select.styled>div {
padding:0!important;
width:100%;
padding: 0 !important;
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;
background:#aaaaaa26!important;
margin:0!important;
ngx-super-select.styled .selection-counter {
color: #000 !important;
background: #aaaaaa26 !important;
margin: 0 !important;
}
.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 {
padding-top: 6px;
padding-bottom: 6px;
}
.mat-mdc-form-field-infix {
min-height: 34px;
padding-top: 6px;
padding-bottom: 6px;
}
.mdc-text-field--filled:not(.mdc-text-field--disabled){
background: transparent!important;
.mat-mdc-form-field-infix {
min-height: 34px;
}
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
background: transparent !important;
}
/* Add application styles & imports to this file! */
pre {
display: flex!important;
margin-top: 0!important;
margin-bottom: 0!important;
word-wrap: break-word!important;
}
code {
flex: 1!important;
line-height: 1.8em!important;
font-size: 14px!important;
min-height: 100%!important;
padding: 1em 1.2em!important;
overflow-x: unset!important;
overflow-y: unset!important;
}
.hljs.hljs-line-numbers {
padding: 0 !important;
}
pre .hljs {
border: none!important;
transition: border ease 1s!important;
}
.hljs-ln {
tr {
&:first-child td {
padding-top: 10px !important;
}
&:last-child td {
padding-bottom: 10px !important;
}
display: flex !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
word-wrap: break-word !important;
}
code {
flex: 1 !important;
line-height: 1.8em !important;
font-size: 14px !important;
min-height: 100% !important;
padding: 1em 1.2em !important;
overflow-x: unset !important;
overflow-y: unset !important;
}
.hljs.hljs-line-numbers {
padding: 0 !important;
}
pre .hljs {
border: none !important;
transition: border ease 1s !important;
}
.hljs-ln {
tr {
&:first-child td {
padding-top: 10px !important;
}
&:last-child td {
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 */
td.hljs-ln-code {
padding-left: 10px !important;
}
}
.gui-structure, .gui-structure *{
font-size: inherit!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 */
td.hljs-ln-code {
padding-left: 10px !important;
}
.gui-structure,
.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

@ -25,4 +25,5 @@ $enable-rtl: true;
// 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": {
"outDir": "./out-tsc/app",
"types": [
"@angular/localize"
"@angular/localize",
"hammerjs"
],
"paths": {
"@docs-components/*": ["./src/components/*"]

View file

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