ui ux enhencment , user tasks improvment , snippets improvment , add sequnces for pro , add wireguard vpn managment for pro, add custom syslog for pro

This commit is contained in:
sepehr 2026-03-28 17:33:41 +03:00
parent 539e8e95fe
commit e95304af3e
22 changed files with 5070 additions and 731 deletions

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

@ -5,18 +5,23 @@ 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: 'VPN Server',
url: '/vpn',
icon: 'fa-solid fa-network-wired'
},
{
name: 'Devices',
url: '/devices',
@ -31,8 +36,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 +75,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 +144,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 +157,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 +172,7 @@ export const navItems: INavData[] = [
{
name: 'Settings',
url: '/settings',
icon: 'fa-solid fa-gear' ,
icon: 'fa-solid fa-gear',
},
// {
// name: 'Backup',
@ -177,7 +194,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

@ -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,62 @@ 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[]){
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 +697,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

@ -0,0 +1,166 @@
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';
peers?: VpnPeer[];
server_config?: VpnServerConfig;
message?: 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,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,590 @@
<c-row>
<c-col xs>
<c-card class="mb-4">
<c-card-header>
<c-row>
<c-col xs [lg]="3">
Alert Sequences (Pro)
</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>
<gui-grid [source]="source" [searching]="searching" [paging]="paging" [columnMenu]="columnMenu"
[sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection" [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="Source Snippet" field="source_snippet_id">
<ng-template let-value="item.source_snippet_id" let-item="item" let-index="index">
Snippet #{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Active" field="is_active">
<ng-template let-value="item.is_active" let-item="item" let-index="index">
<i *ngIf="value" class="fa-solid fa-check" style="color: green;"></i>
<i *ngIf="!value" class="fa-solid fa-x" style="color: red;"></i>
</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="success" size="sm" (click)="Run_Sequence(item)" class=""><i
class="fa-solid fa-play mx-1"></i>Execute</button>
<button cButton color="primary" size="sm" (click)="Edit_Sequence(item,'edit')" class="mx-1"><i
class="fa-regular fa-pen-to-square mx-1"></i>Edit</button>
<button cButton color="info" size="sm" (click)="show_history(item)" class=""><i
class="fa-solid fa-clock-rotate-left mx-1"></i>History</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)" class="mx-1"><i
class="fa-regular fa-trash-can mx-1"></i>Delete</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</c-card-body>
</c-card>
</c-col>
</c-row>
<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">
<gui-grid [source]="run.devices"
[searching]="searchingHistory"
[paging]="pagingHistory"
[sorting]="sorting"
[infoPanel]="false"
[autoResizeWidth]="true"
class="border-0">
<gui-grid-column header="Device" field="device_id">
<ng-template let-item="item">
<div class="fw-semibold ms-2">
<i class="fa-solid fa-microchip me-2 opacity-50"></i>Device #{{item.device_id}}
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Status" field="status" align="center" width="150">
<ng-template let-item="item">
<c-badge [color]="item.status === 'success' ? 'success' : 'danger'" class="text-white px-2">
{{item.status | uppercase}}
</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" align="right" width="150">
<ng-template let-item="item">
<button cButton color="dark" size="sm" variant="outline" class="me-2" (click)="showTrace(item)">
<i class="fa-solid fa-terminal me-1"></i> Trace Log
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</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>
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [rowSelection]="rowSelection" [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_sequence['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>
<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>
<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="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_sequence['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_sequence['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>
<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,429 @@
import { Component, OnInit } from '@angular/core';
import { dataProvider } from "../../providers/mikrowizard/data";
import { ToastComponent } from '@coreui/angular';
import { NgxSuperSelectOptions } from "ngx-super-select";
@Component({
selector: 'app-sequences',
templateUrl: './sequences.component.html',
styleUrls: ['./sequences.component.scss']
})
export class SequencesComponent implements OnInit {
public sequences: any[] = [];
// Grid config
source: Array<any> = [];
searching = {
enabled: true,
placeholder: 'Search sequences...'
};
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 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 searchingMembers = {
enabled: true,
placeholder: 'Search members...'
};
// Device History Grid Config
searchingHistory = {
enabled: true,
placeholder: 'Search devices in this run...'
};
pagingHistory = {
enabled: true,
pageSize: 5,
pageSizes: [5, 10, 25]
};
// 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,50 @@
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 { GuiGridModule } from "@generic-ui/ngx-grid";
import { NgxSuperSelectModule } from "ngx-super-select";
@NgModule({
imports: [
SequencesRoutingModule,
CardModule,
CommonModule,
GridModule,
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
ModalModule,
ToastModule,
FormsModule,
BadgeModule,
NgxSuperSelectModule,
AccordionModule,
NavModule,
TabsModule,
AlertModule,
HighlightJsModule,
CollapseModule
],
declarations: [SequencesComponent],
})
export class SequencesModule { }

View file

@ -38,6 +38,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
public uid: number;
public uname: string;
public tz: string;
public ispro: boolean = false;
constructor(
private data_provider: dataProvider,
@ -56,7 +57,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;
@ -182,31 +184,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() {
@ -258,7 +260,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
this.NewMemberRows = rows;
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
}
add_new_members() {
var _self = this;
_self.SelectedMembers = [
@ -274,12 +276,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 +289,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";
}
@ -318,7 +320,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 +332,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 +351,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 +363,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 +378,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 +407,5 @@ export class SnippetsComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {}
ngOnDestroy(): void { }
}

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,537 @@
<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>
<gui-grid [source]="source" [searching]="searching" [paging]="paging" [columnMenu]="columnMenu"
[sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection" [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="Regex Pattern" field="regex_pattern">
<ng-template let-value="item.regex_pattern" let-item="item" let-index="index">
<code class="px-1 py-1">{{value}}</code>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Alert / Severity" field="alert_id">
<ng-template let-value="item.alert_id" let-item="item" let-index="index">
<div class="d-flex flex-row gap-1">
<span class="fw-bold">{{ getAlertName(value) }}</span>
<c-badge size="sm" [color]="getAlertLevel(value).toLowerCase() =='critical' ? 'danger' :
getAlertLevel(value).toLowerCase() =='warning' ? 'warning' :
getAlertLevel(value).toLowerCase() =='info' ? 'info' :
getAlertLevel(value).toLowerCase() =='error' ? 'danger' : 'secondary'">{{
getAlertLevel(value)}}</c-badge>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Alert Enabled/Type" field="alert_enabled" align="center" width="150">
<ng-template let-value="item.alert_enabled" let-item="item" let-index="index">
<i *ngIf="value" class="fa-solid fa-check mx-1" style="color: green;"></i>
<i *ngIf="!value" 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>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Match String" field="match_string">
<ng-template let-value="item.match_string" 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" width="200">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="primary" size="sm" (click)="Edit_Regex(item,'edit')" class="mx-1"><i
class="fa-regular fa-pen-to-square mx-1"></i>Edit</button>
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)"
class="mx-1"><i class="fa-regular fa-trash-can mx-1"></i>Delete</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</c-card-body>
</c-card>
</c-col>
</c-row>
<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,114 @@
.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,584 @@
import { Component, OnInit } from '@angular/core';
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 {
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();
}
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,50 @@
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 { GuiGridModule } from "@generic-ui/ngx-grid";
import { NgxSuperSelectModule } from "ngx-super-select";
@NgModule({
imports: [
SyslogRegexRoutingModule,
CardModule,
CommonModule,
GridModule,
FormModule,
ButtonModule,
ButtonGroupModule,
GuiGridModule,
ModalModule,
ToastModule,
FormsModule,
BadgeModule,
NgxSuperSelectModule,
AccordionModule,
NavModule,
TabsModule,
AlertModule,
HighlightJsModule,
CollapseModule
],
declarations: [SyslogRegexComponent],
})
export class SyslogRegexModule { }

View file

@ -19,8 +19,9 @@
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<i *ngIf="item.task_type=='snippet'" class="fa-solid fa-code"></i>
<i *ngIf="item.task_type=='backup'" class="fa-solid fa-database"></i>
<i *ngIf="item.task_type=='firmware'" class="fa-solid fa-upload"></i>
<i *ngIf="item.task_type=='backup'" class="fa-solid fa-database"></i>
<i *ngIf="item.task_type=='firmware'" class="fa-solid fa-upload"></i>
<i *ngIf="item.task_type=='sequence'" class="fa-solid fa-code-branch"></i>
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column header="Description" field="description">
@ -56,257 +57,278 @@
</c-col>
</c-row>
<c-modal #EditTaskModal backdrop="static" size="xl" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<c-modal-header class="bg-light">
<h5 *ngIf="SelectedTask['action']=='edit'" cModalTitle>
<i class="fa-solid fa-edit me-2"></i>Edit Task: {{SelectedTask['name']}}
</h5>
<h5 *ngIf="SelectedTask['action']=='add'" cModalTitle>
<i class="fa-solid fa-plus me-2"></i>Create New Task
</h5>
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4">
<!-- Basic Information Section -->
<div class="task-form-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
<small class="text-muted">Define the task name, description and type</small>
</div>
<c-row class="g-3">
<c-col xs="12" md="6">
<input cFormControl placeholder="Task Name (required)" [(ngModel)]="SelectedTask['name']"
class="form-input" title="Unique name for this task" />
</c-col>
<c-col xs="12" md="6">
<select cSelect [(ngModel)]="SelectedTask['task_type']" (change)="onTaskTypeChange()" class="form-select" title="Select task type">
<option value="">Choose Task Type...</option>
<option value="backup">📁 Backup Configuration</option>
<option value="snippet">📝 Execute Script/Snippet</option>
<option value="firmware" *ngIf="ispro">🔄 Firmware Update</option>
</select>
</c-col>
<c-col xs="12">
<textarea cFormControl placeholder="Task Description (optional)" [(ngModel)]="SelectedTask['description']"
class="form-input" rows="2" title="Brief description of what this task does"></textarea>
</c-col>
</c-row>
<c-modal #EditTaskModal backdrop="static" size="xl" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<c-modal-header class="bg-light">
<h5 *ngIf="SelectedTask['action']=='edit'" cModalTitle>
<i class="fa-solid fa-edit me-2"></i>Edit Task: {{SelectedTask['name']}}
</h5>
<h5 *ngIf="SelectedTask['action']=='add'" cModalTitle>
<i class="fa-solid fa-plus me-2"></i>Create New Task
</h5>
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body class="p-4">
<!-- Basic Information Section -->
<div class="task-form-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
<small class="text-muted">Define the task name, description and type</small>
</div>
<!-- Task Configuration Section -->
<div class="task-config-section mb-4" *ngIf="SelectedTask['task_type']">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-cog me-2"></i>Task Configuration</h6>
<small class="text-muted">Configure task-specific settings and parameters</small>
</div>
<!-- Backup Configuration -->
<div *ngIf="SelectedTask['task_type']=='backup'" class="backup-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-success text-white">
<h6 class="mb-0"><i class="fa-solid fa-database me-2"></i>Backup Configuration</h6>
</c-card-header>
<c-card-body>
<div class="alert alert-info">
<i class="fa-solid fa-info-circle me-2"></i>
This task will create configuration backups of selected devices. Backups are stored securely and can be restored later.
</div>
</c-card-body>
</c-card>
</div>
<!-- Firmware Configuration -->
<c-card *ngIf="SelectedTask['task_type']=='firmware'" class="mb-3">
<c-card-header class="bg-warning text-dark">
<h6 class="mb-0"><i class="fa-solid fa-microchip me-2"></i>Firmware Update Strategy</h6>
<c-row class="g-3">
<c-col xs="12" md="6">
<input cFormControl placeholder="Task Name (required)" [(ngModel)]="SelectedTask['name']" class="form-input"
title="Unique name for this task" />
</c-col>
<c-col xs="12" md="6">
<select cSelect [(ngModel)]="SelectedTask['task_type']" (change)="onTaskTypeChange()" class="form-select"
title="Select task type">
<option value="">Choose Task Type...</option>
<option value="backup">📁 Backup Configuration</option>
<option value="snippet">📝 Execute Script/Snippet</option>
<option value="firmware" *ngIf="ispro">🔄 Firmware Update</option>
<option value="sequence" *ngIf="ispro">⛓️ Alert Sequence</option>
</select>
</c-col>
<c-col xs="12">
<textarea cFormControl placeholder="Task Description (optional)" [(ngModel)]="SelectedTask['description']"
class="form-input" rows="2" title="Brief description of what this task does"></textarea>
</c-col>
</c-row>
</div>
<!-- Task Configuration Section -->
<div class="task-config-section mb-4" *ngIf="SelectedTask['task_type']">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-cog me-2"></i>Task Configuration</h6>
<small class="text-muted">Configure task-specific settings and parameters</small>
</div>
<!-- Backup Configuration -->
<div *ngIf="SelectedTask['task_type']=='backup'" class="backup-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-success text-white">
<h6 class="mb-0"><i class="fa-solid fa-database me-2"></i>Backup Configuration</h6>
</c-card-header>
<c-card-body>
<div class="strategy-buttons mb-3">
<c-button-group role="group" class="w-100">
<button cButton [active]="SelectedTask['data']['strategy']=='system'"
(click)="firmware_type_changed('system')" color="info" variant="outline" class="flex-fill">
<i class="fa-solid fa-cogs me-1"></i>System Default
</button>
<button cButton [active]="SelectedTask['data']['strategy']=='defined'"
(click)="firmware_type_changed('defined')" color="warning" variant="outline" class="flex-fill">
<i class="fa-solid fa-list me-1"></i>Custom Version
</button>
<button cButton [active]="SelectedTask['data']['strategy']=='latest'"
(click)="firmware_type_changed('latest')" color="success" variant="outline" class="flex-fill">
<i class="fa-solid fa-download me-1"></i>Latest Available
</button>
</c-button-group>
</div>
<div *ngIf=" SelectedTask['data']['strategy']=='system'" class="alert alert-info">
<div class="alert alert-info">
<i class="fa-solid fa-info-circle me-2"></i>
Uses global MikroWizard update strategy settings. Check Settings page for configuration.
</div>
<div *ngIf="firms_loaded && SelectedTask['data']['strategy']=='latest'" class="alert alert-success">
<i class="fa-solid fa-cloud-download-alt me-2"></i>
Downloads latest firmware from mikrotik.com. Server needs internet access.
</div>
<div *ngIf="firms_loaded && SelectedTask['data']['strategy']=='defined'">
<c-row class="g-3">
<c-col xs="12" md="6">
<label class="form-label">RouterOS v7+ Version</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install']" class="form-select">
<option value="">Choose version...</option>
<option *ngFor="let f of available_firmwares" [value]="f">{{f}}</option>
</select>
</c-col>
<c-col xs="12" md="6" *ngIf="updateBehavior=='keep'">
<label class="form-label">RouterOS v6 Version</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install_6']" class="form-select">
<option value="">Choose version...</option>
<option *ngFor="let f of available_firmwaresv6" [value]="f">{{f}}</option>
</select>
</c-col>
</c-row>
This task will create configuration backups of selected devices. Backups are stored securely and can be
restored later.
</div>
</c-card-body>
</c-card>
<!-- Snippet Configuration -->
<div *ngIf="SelectedTask['task_type']=='snippet'" class="snippet-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-primary text-white">
<h6 class="mb-0"><i class="fa-solid fa-code me-2"></i>Script/Snippet Configuration</h6>
</c-card-header>
<c-card-body>
<label class="form-label">Select Script/Snippet to Execute</label>
<ngx-super-select [dataSource]="Snippets" [options]="options"
(selectionChanged)="onSelectValueChanged($event)" [selectedItemValues]="[SelectedTask['snippetid']]"
(searchChanged)="onSnippetsValueChanged($event)" class="styled"></ngx-super-select>
<small class="text-muted mt-2 d-block">
<i class="fa-solid fa-info-circle me-1"></i>
The selected script will be executed on all target devices when this task runs.
</small>
</c-card-body>
</c-card>
</div>
</div>
<!-- Schedule Configuration -->
<div class="schedule-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-clock me-2"></i>Schedule Configuration</h6>
<small class="text-muted">Set when this task should run automatically</small>
</div>
<div class="cron-input-wrapper">
<label class="form-label">Execution Schedule (Cron Expression)</label>
<div class="search-select-wrapper">
<div class="input-group">
<input cFormControl
[(ngModel)]="SelectedTask['cron']"
(input)="onCronInputChange($event)"
(focus)="onCronInputFocus()"
(blur)="hideCronDropdown()"
placeholder="Enter cron expression or search presets..."
class="search-input"
autocomplete="off" />
<button class="btn btn-outline-secondary" type="button" (click)="onCronInputFocus()" title="Show presets">
<i class="fa-solid fa-clock"></i>
</button>
</div>
<div *ngIf="showCronDropdown && filteredCrons.length > 0" class="search-dropdown cron-dropdown">
<div *ngFor="let cron of filteredCrons"
class="search-option cron-option"
[class.selected]="selectedCronPreset?.value === cron.value"
(mousedown)="selectCron(cron)">
<div class="cron-label">{{cron.label}}</div>
<div class="cron-value">{{cron.value}}</div>
<div class="cron-description">{{cron.description}}</div>
</div>
</div>
<div *ngIf="showCronDropdown && filteredCrons.length === 0 && cronSearch" class="search-no-results">
No matching cron presets found
</div>
</div>
<small class="text-muted mt-1 d-block">
<i class="fa-solid fa-info-circle me-1"></i>{{getCronDescription()}}
</small>
<div class="mt-2">
<small class="text-muted">
<strong>Quick Examples:</strong>
<code class="me-2">* * * * *</code> = every minute |
<code class="me-2">0 2 * * *</code> = daily at 2 AM |
<code class="me-2">0 */6 * * *</code> = every 6 hours
</small>
</div>
</div>
</div>
<!-- Target Selection -->
<div class="target-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-bullseye me-2"></i>Target Selection</h6>
<small class="text-muted">Choose which devices or groups this task will affect</small>
</div>
<div class="target-type-selector mb-3">
<c-button-group role="group" class="w-100">
<button cButton [active]="SelectedTask['selection_type']=='devices'"
(click)="SelectedTask['selection_type']='devices'; form_changed()"
color="primary" variant="outline" class="flex-fill">
<i class="fa-solid fa-server me-1"></i>Individual Devices
</button>
<button cButton [active]="SelectedTask['selection_type']=='groups'"
(click)="SelectedTask['selection_type']='groups'; form_changed()"
color="info" variant="outline" class="flex-fill">
<i class="fa-solid fa-layer-group me-1"></i>Device Groups
</button>
</c-button-group>
</div>
<!-- Selected Targets -->
<c-card>
<c-card-header class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fa-solid fa-list me-2"></i>Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}
</h6>
<div>
<c-badge color="info" class="me-2">{{SelectedMembers.length}} selected</c-badge>
<button cButton color="success" size="sm" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}
<!-- Firmware Configuration -->
<c-card *ngIf="SelectedTask['task_type']=='firmware'" class="mb-3">
<c-card-header class="bg-warning text-dark">
<h6 class="mb-0"><i class="fa-solid fa-microchip me-2"></i>Firmware Update Strategy</h6>
</c-card-header>
<c-card-body>
<div class="strategy-buttons mb-3">
<c-button-group role="group" class="w-100">
<button cButton [active]="SelectedTask['data']['strategy']=='system'"
(click)="firmware_type_changed('system')" color="info" variant="outline" class="flex-fill">
<i class="fa-solid fa-cogs me-1"></i>System Default
</button>
</div>
<button cButton [active]="SelectedTask['data']['strategy']=='defined'"
(click)="firmware_type_changed('defined')" color="warning" variant="outline" class="flex-fill">
<i class="fa-solid fa-list me-1"></i>Custom Version
</button>
<button cButton [active]="SelectedTask['data']['strategy']=='latest'"
(click)="firmware_type_changed('latest')" color="success" variant="outline" class="flex-fill">
<i class="fa-solid fa-download me-1"></i>Latest Available
</button>
</c-button-group>
</div>
<div *ngIf=" SelectedTask['data']['strategy']=='system'" class="alert alert-info">
<i class="fa-solid fa-info-circle me-2"></i>
Uses global MikroWizard update strategy settings. Check Settings page for configuration.
</div>
<div *ngIf="firms_loaded && SelectedTask['data']['strategy']=='latest'" class="alert alert-success">
<i class="fa-solid fa-cloud-download-alt me-2"></i>
Downloads latest firmware from mikrotik.com. Server needs internet access.
</div>
<div *ngIf="firms_loaded && SelectedTask['data']['strategy']=='defined'">
<c-row class="g-3">
<c-col xs="12" md="6">
<label class="form-label">RouterOS v7+ Version</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install']" class="form-select">
<option value="">Choose version...</option>
<option *ngFor="let f of available_firmwares" [value]="f">{{f}}</option>
</select>
</c-col>
<c-col xs="12" md="6" *ngIf="updateBehavior=='keep'">
<label class="form-label">RouterOS v6 Version</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install_6']" class="form-select">
<option value="">Choose version...</option>
<option *ngFor="let f of available_firmwaresv6" [value]="f">{{f}}</option>
</select>
</c-col>
</c-row>
</div>
</c-card-body>
</c-card>
<!-- Snippet Configuration -->
<div *ngIf="SelectedTask['task_type']=='snippet'" class="snippet-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-primary text-white">
<h6 class="mb-0"><i class="fa-solid fa-code me-2"></i>Script/Snippet Configuration</h6>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="SelectedMembers.length === 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} fa-3x mb-3 opacity-50"></i>
<h6>No {{SelectedTask['selection_type']}} selected</h6>
<p class="mb-0">Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to select targets for this task</p>
</div>
<div *ngIf="SelectedMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [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-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</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">
<small class="text-muted">{{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 color="danger" size="sm" variant="outline" (click)="remove_member(item)" title="Remove from task">
<i class="fa-solid fa-times"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
<c-card-body>
<label class="form-label">Select Script/Snippet to Execute</label>
<ngx-super-select [dataSource]="Snippets" [options]="options"
(selectionChanged)="onSelectValueChanged($event)" [selectedItemValues]="[SelectedTask['snippetid']]"
(searchChanged)="onSnippetsValueChanged($event)" class="styled"></ngx-super-select>
<small class="text-muted mt-2 d-block">
<i class="fa-solid fa-info-circle me-1"></i>
The selected script will be executed on all target devices when this task runs.
</small>
</c-card-body>
</c-card>
</div>
<!--
<!-- Sequence Configuration -->
<div *ngIf="SelectedTask['task_type']=='sequence'" class="sequence-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-primary text-white">
<h6 class="mb-0"><i class="fa-solid fa-code-branch me-2"></i>Sequence Configuration</h6>
</c-card-header>
<c-card-body>
<label class="form-label">Select Alert Sequence</label>
<ngx-super-select [dataSource]="Sequences" [options]="seqOptions"
(selectionChanged)="onSequenceSelected($event)" [selectedItemValues]="[SelectedTask['data']?.sequence_id]"
(searchChanged)="onSequencesSearchChanged($event)" class="styled"></ngx-super-select>
<small class="text-muted mt-2 d-block">
<i class="fa-solid fa-info-circle me-1"></i>
The selected alert sequence will be executed on all target devices when this task runs.
</small>
</c-card-body>
</c-card>
</div>
</div>
<!-- Schedule Configuration -->
<div class="schedule-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-clock me-2"></i>Schedule Configuration</h6>
<small class="text-muted">Set when this task should run automatically</small>
</div>
<div class="cron-input-wrapper">
<label class="form-label">Execution Schedule (Cron Expression)</label>
<div class="search-select-wrapper">
<div class="input-group">
<input cFormControl [(ngModel)]="SelectedTask['cron']" (input)="onCronInputChange($event)"
(focus)="onCronInputFocus()" (blur)="hideCronDropdown()"
placeholder="Enter cron expression or search presets..." class="search-input" autocomplete="off" />
<button class="btn btn-outline-secondary" type="button" (click)="onCronInputFocus()" title="Show presets">
<i class="fa-solid fa-clock"></i>
</button>
</div>
<div *ngIf="showCronDropdown && filteredCrons.length > 0" class="search-dropdown cron-dropdown">
<div *ngFor="let cron of filteredCrons" class="search-option cron-option"
[class.selected]="selectedCronPreset?.value === cron.value" (mousedown)="selectCron(cron)">
<div class="cron-label">{{cron.label}}</div>
<div class="cron-value">{{cron.value}}</div>
<div class="cron-description">{{cron.description}}</div>
</div>
</div>
<div *ngIf="showCronDropdown && filteredCrons.length === 0 && cronSearch" class="search-no-results">
No matching cron presets found
</div>
</div>
<small class="text-muted mt-1 d-block">
<i class="fa-solid fa-info-circle me-1"></i>{{getCronDescription()}}
</small>
<div class="mt-2">
<small class="text-muted">
<strong>Quick Examples:</strong>
<code class="me-2">* * * * *</code> = every minute |
<code class="me-2">0 2 * * *</code> = daily at 2 AM |
<code class="me-2">0 */6 * * *</code> = every 6 hours
</small>
</div>
</div>
</div>
<!-- Target Selection -->
<div class="target-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-bullseye me-2"></i>Target Selection</h6>
<small class="text-muted">Choose which devices or groups this task will affect</small>
</div>
<div class="target-type-selector mb-3">
<c-button-group role="group" class="w-100">
<button cButton [active]="SelectedTask['selection_type']=='devices'"
(click)="SelectedTask['selection_type']='devices'; form_changed()" color="primary" variant="outline"
class="flex-fill">
<i class="fa-solid fa-server me-1"></i>Individual Devices
</button>
<button cButton [active]="SelectedTask['selection_type']=='groups'"
(click)="SelectedTask['selection_type']='groups'; form_changed()" color="info" variant="outline"
class="flex-fill">
<i class="fa-solid fa-layer-group me-1"></i>Device Groups
</button>
</c-button-group>
</div>
<!-- Selected Targets -->
<c-card>
<c-card-header class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fa-solid fa-list me-2"></i>Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' :
'Groups'}}
</h6>
<div>
<c-badge color="info" class="me-2">{{SelectedMembers.length}} selected</c-badge>
<button cButton color="success" size="sm" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' :
'Groups'}}
</button>
</div>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="SelectedMembers.length === 0" class="text-center p-4 text-muted">
<i
class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} fa-3x mb-3 opacity-50"></i>
<h6>No {{SelectedTask['selection_type']}} selected</h6>
<p class="mb-0">Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to
select targets for this task</p>
</div>
<div *ngIf="SelectedMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [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-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</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">
<small class="text-muted">{{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 color="danger" size="sm" variant="outline" (click)="remove_member(item)"
title="Remove from task">
<i class="fa-solid fa-times"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
</c-card-body>
</c-card>
</div>
<!--
<c-input-group class="mb-3">
<ngx-super-select
[dataSource]="Members"
@ -319,178 +341,183 @@
</ngx-super-select>
</c-input-group> -->
<!-- <ng-container *ngIf="SelectedMembers.length>0 && EditTaskModalVisible">
<!-- <ng-container *ngIf="SelectedMembers.length>0 && EditTaskModalVisible">
<c-badge class="mx-1" *ngFor="let id of splitids(SelectedTaskItems)" color="dark">{{get_member_by_id(id).name}}</c-badge>
</ng-container> -->
<!--
<!--
<c-input-group class="mb-3">
<cron-editor #cronEditorDemo1 [(ngModel)]="SelectedTask['cron']" [options]="cronOptions">Cron here...</cron-editor>
</c-input-group>
-->
</c-modal-body>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">All fields marked with * are required</small>
</div>
<div>
<button *ngIf="SelectedTask['action']=='add'" (click)="submit('add')" cButton color="primary">
<i class="fa-solid fa-plus me-1"></i>Create Task
</button>
<button *ngIf="SelectedTask['action']=='edit'" (click)="submit('edit')" cButton color="primary">
<i class="fa-solid fa-save me-1"></i>Save Changes
</button>
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" class="ms-2">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</div>
</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-plus-circle me-2"></i>Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} to Task
</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> {{SelectedTask['selection_type']}}(s) selected for addition to this task</span>
</c-alert>
</div>
<!-- Available Items -->
<c-card>
<c-card-header>
<h6 class="mb-0">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2"></i>
Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{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 {{SelectedTask['selection_type']}} are already assigned</h6>
<p class="mb-0">No available {{SelectedTask['selection_type']}} to add to this task</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)" [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-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</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">
<c-badge color="secondary">{{value}}</c-badge>
</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">
<small class="text-muted">{{value}}</small>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">Select {{SelectedTask['selection_type']}} from the list above to add them to this task</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}} {{SelectedTask['selection_type']}}(s)
</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>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedTask['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>{{ SelectedTask['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedTask['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedTask['desc_cron'] }}</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="confirm_delete('',true)" cButton color="danger">
Yes,Delete!
</c-modal-body>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">All fields marked with * are required</small>
</div>
<div>
<button *ngIf="SelectedTask['action']=='add'" (click)="submit('add')" cButton color="primary">
<i class="fa-solid fa-plus me-1"></i>Create Task
</button>
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info">
Close
<button *ngIf="SelectedTask['action']=='edit'" (click)="submit('edit')" cButton color="primary">
<i class="fa-solid fa-save me-1"></i>Save Changes
</button>
</c-modal-footer>
</c-modal>
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" class="ms-2">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</div>
</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-plus-circle me-2"></i>Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' :
'Groups'}} to Task
</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> {{SelectedTask['selection_type']}}(s) selected for addition to
this task</span>
</c-alert>
</div>
<!-- Available Items -->
<c-card>
<c-card-header>
<h6 class="mb-0">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2"></i>
Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{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 {{SelectedTask['selection_type']}} are already assigned</h6>
<p class="mb-0">No available {{SelectedTask['selection_type']}} to add to this task</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)" [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-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</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">
<c-badge color="secondary">{{value}}</c-badge>
</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">
<small class="text-muted">{{value}}</small>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">Select {{SelectedTask['selection_type']}} from the list above to add them to this
task</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}} {{SelectedTask['selection_type']}}(s)
</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>
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm delete {{ SelectedTask['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>{{ SelectedTask['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedTask['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedTask['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-modal #runConfirmModal backdrop="static" [(visible)]="runConfirmModalVisible" id="runConfirmModal">
<c-modal-header>
<h6 cModalTitle>Confirm RUN {{ SelectedTask['name'] }}</h6>
<button [cModalToggle]="runConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
Are you sure that You want to run following task ?
<br />
<br />
<table style="width: 100%;">
<tr>
<td><b>Taks name : </b></td>
<td>{{ SelectedTask['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedTask['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedTask['desc_cron'] }}</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="confirm_delete" cButton color="danger">
Yes,Run!
</button>
<button [cModalToggle]="runConfirmModal.id" cButton color="info">
Close
</button>
</c-modal-footer>
</c-modal>
<c-modal #runConfirmModal backdrop="static" [(visible)]="runConfirmModalVisible" id="runConfirmModal">
<c-modal-header>
<h6 cModalTitle>Confirm RUN {{ SelectedTask['name'] }}</h6>
<button [cModalToggle]="runConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
Are you sure that You want to run following task ?
<br />
<br />
<table style="width: 100%;">
<tr>
<td><b>Taks name : </b></td>
<td>{{ SelectedTask['name'] }}</td>
</tr>
<tr>
<td><b>Description : </b></td>
<td>{{ SelectedTask['description'] }}</td>
</tr>
<tr>
<td><b>Cron exec : </b></td>
<td>{{ SelectedTask['desc_cron'] }}</td>
</tr>
</table>
</c-modal-body>
<c-modal-footer>
<button (click)="confirm_delete" cButton color="danger">
Yes,Run!
</button>
<button [cModalToggle]="runConfirmModal.id" cButton color="info">
Close
</button>
</c-modal-footer>
</c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -77,6 +77,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 +97,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 +105,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 +114,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' },
@ -156,6 +157,16 @@ export class UserTasksComponent implements OnInit {
enableDarkMode: false,
};
seqOptions: Partial<NgxSuperSelectOptions> = {
selectionMode: "single",
actionsEnabled: false,
displayExpr: "name",
valueExpr: "id",
placeholder: "Sequence",
searchEnabled: true,
enableDarkMode: false,
};
public paging: GuiPaging = {
enabled: true,
page: 1,
@ -299,15 +310,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 +341,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 +374,7 @@ export class UserTasksComponent implements OnInit {
}
firmware_type_changed(type: any) {
this.SelectedTask['data']['strategy'] = type;
if (type == 'system') {
@ -405,7 +425,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 +490,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 +511,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 +520,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 +533,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 +542,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 +555,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

@ -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,621 @@
<div class="fade-in">
<!-- 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', 'bg-danger text-white': status?.status === 'error' || !status, 'bg-warning text-dark': status?.status === 'setup_required'}">
<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">
{{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>
<gui-grid #grid [rowClass]="rowClass" [source]="source" [searching]="searching" [paging]="paging"
[columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [autoResizeWidth]="true"
[loading]="loading">
<gui-grid-column header="Status" field="status" [width]="50" align="center">
<ng-template let-item="item">
<i *ngIf="item.status === 'online'" class="fa-solid fa-circle text-success" cTooltip="Online"></i>
<i *ngIf="item.status === 'offline'" class="fa-solid fa-circle text-danger" cTooltip="Offline"></i>
<i *ngIf="item.status === 'unreachable'" class="fa-solid fa-triangle-exclamation text-warning"
cTooltip="Unreachable (Handshake OK, Ping Failed)"></i>
<i *ngIf="!item.status && item.is_enabled" class="fa-solid fa-circle text-info" cTooltip="Enabled"></i>
<i *ngIf="!item.status && !item.is_enabled" class="fa-solid fa-circle text-secondary"
cTooltip="Disabled"></i>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Identity & IP" field="name" [width]="220">
<ng-template let-item="item">
<div style="line-height: 1.2;">
<i *ngIf="item.mt_user" class="fas fa-server text-info me-1" cTooltip="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>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Details & Description" field="description">
<ng-template let-item="item">
<div class="d-flex flex-column justify-content-center w-100">
<!-- Description (Bigger if exists, hidden if empty) -->
<div *ngIf="item.description" class="text-muted mb-1 text-wrap"
style="font-size: 0.88rem; line-height: 1.3; font-weight: 500;" [title]="item.description">
{{ item.description }}
</div>
<!-- Peer Info (Horizontal flex, smaller if Description exists) -->
<div style="color: #adb5bd;" class="d-flex flex-row align-items-center gap-3"
[ngStyle]="{'font-size': item.description ? '0.7rem' : '0.8rem', 'margin-top': item.description ? '4px' : '0'}">
<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>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Routing & Identity" field="nat_mode" [width]="170">
<ng-template let-value="item.nat_mode" let-item="item">
<div class="mb-1 mx-1">
<c-badge size="sm"
[color]="value === 'full' ? 'success' : (value === 'split' ? 'info' : 'secondary')">
{{ value | uppercase }}
</c-badge>
</div>
<div style="font-size: 0.75rem;margin-top: 5px; " class="text-muted">
<i class="fas fa-network-wired"></i> Iface: {{ item.custom_interface || 'Auto' }}
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Integration" align="center" field="linked_device_id" [width]="250">
<ng-template let-item="item">
<div class="mb-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.75rem; transition: all 0.2s;">
<i class="fa-solid fa-link"></i> View Device
</a>
<div class="d-flex flex-row justify-content-center w-100 ng-star-inserted">
<small *ngIf="!item.linked_device_id && !item.scan_status && item.mt_user" class="text-muted">Device
Not Linked</small>
<!-- Scan Status Micro-animations -->
<div style="font-size: 0.75rem;"
*ngIf="item.scan_status &&item.scan_status === 'starting' || item.scan_status === 'running'"
class="text-info mx-1 mt-1">
<i class="fas fa-circle-notch fa-spin me-1"></i> Scanning...
</div>
<div style="font-size: 0.75rem;" *ngIf="item.scan_status && item.scan_status === 'completed'"
class="text-success mx-1 mt-1" style="animation: fadeIn 0.5s ease-in-out;">
<i class="fas fa-check-circle me-1"></i> Scan Completed
</div>
<div style="font-size: 0.75rem;" *ngIf="item.scan_status &&item.scan_status === 'failed'"
class="text-danger mx-1 mt-1" cTooltip="MikroTik connection or credential validation failed">
<i class="fas fa-exclamation-triangle me-1"></i> Scan Failed
</div>
<div style="font-size: 0.75rem;" class="text-secondary mx-1 mt-1">
<i class="fas fa-heartbeat"></i> Keepalive: {{ item.persistent_keepalive }}s
</div>
</div>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" header="Last Handshake" field="stats" [width]="150">
<ng-template let-item="item">
<div *ngIf="item.stats" style="font-size: 0.85rem;" class="mt-1">
<span *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>
</span>
<span *ngIf="!item.stats.last_handshake || item.stats.last_handshake === 0" class="text-muted">
<i class="fas fa-handshake"></i> None
</span>
</div>
<span *ngIf="!item.stats" class="text-muted text-sm">-</span>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" header="Transfer Speed" field="stats" [width]="130">
<ng-template let-item="item">
<div *ngIf="item.stats" class="mt-1 flex-column d-flex align-items-start"
style="font-size: 0.75rem; font-family: monospace; font-weight: bold;">
<span [ngStyle]="{'color': item.stats.rx_speed !== 0 ? '#2eb85c' : '#8a93a2'}"
style="transition: color 0.3s ease;">
<i class="fas fa-arrow-down opacity-75"></i> {{ formatBytes(item.stats.rx_speed || 0) }}/s
</span>
<span [ngStyle]="{'color': item.stats.tx_speed !== 0 ? '#3399ff' : '#8a93a2'}" class="mt-1"
style="transition: color 0.3s ease;">
<i class="fas fa-arrow-up opacity-75"></i> {{ formatBytes(item.stats.tx_speed || 0) }}/s
</span>
</div>
<span *ngIf="!item.stats" class="text-muted text-sm">-</span>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" header="Total Volume" field="stats" [width]="120">
<ng-template let-item="item">
<div *ngIf="item.stats" class="mt-1 flex-column d-flex align-items-start" style="font-size: 0.75rem;">
<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 mt-1"><i class="fas fa-arrow-up opacity-50"></i> {{(item.stats.tx_bytes /
1048576) | number:'1.2-2'}} MB</span>
</div>
<span *ngIf="!item.stats" class="text-muted text-sm">-</span>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" header="State" field="is_enabled" [width]="120">
<ng-template let-item="item">
<div class="mt-2 d-flex justify-content-center">
<c-form-check [switch]="true">
<input cFormCheckInput type="checkbox" [checked]="item.is_enabled"
(click)="$event.preventDefault(); promptToggleEnabled(item)"
[disabled]="item.is_managed === false" [id]="'switch-peer-' + item.id" />
<label cFormCheckLabel [for]="'switch-peer-' + item.id"
[ngClass]="item.is_enabled ? 'text-success fw-bold' : 'text-secondary'"
style="cursor: pointer; font-size: 0.85rem;">
{{ item.is_enabled ? 'Enabled' : 'Disabled' }}
</label>
</c-form-check>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" header="" field="_search_index" [enabled]="false" [width]="0">
<ng-template let-item="item"></ng-template>
</gui-grid-column>
<gui-grid-column align="center" [width]="80" [cellEditing]="false" [sorting]="false" header="Actions">
<ng-template let-item="item">
<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>
</ng-template>
</gui-grid-column>
</gui-grid>
</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>
<!-- End of File -->
</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,593 @@
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 {
GuiGridComponent,
GuiRowClass,
GuiSearching,
GuiColumn,
GuiColumnMenu,
GuiPaging,
GuiPagingDisplay,
GuiInfoPanel
} from "@generic-ui/ngx-grid";
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("grid", { static: true }) gridComponent!: GuiGridComponent;
@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;
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;
rowClass: GuiRowClass = { class: "row-highlighted" };
searching: GuiSearching = { enabled: true, placeholder: "Search Peers" };
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 sorting = { enabled: true, multiSorting: true };
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.source = (res.peers || []).map(p => ({
...p,
_search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}`
}));
this.computeTotals();
this.loading = false;
} 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.source = (res.peers || []).map(p => ({
...p,
_search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}`
}));
this.computeTotals();
}
},
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,71 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { GuiGridModule } from '@generic-ui/ngx-grid';
import { ChartjsModule } from '@coreui/angular-chartjs';
import { HighlightJsModule } from 'ngx-highlight-js';
import { ClipboardModule } from '@angular/cdk/clipboard';
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';
@NgModule({
imports: [
VpnRoutingModule,
CardModule,
NavModule,
IconModule,
TabsModule,
CommonModule,
GuiGridModule,
ProgressModule,
ReactiveFormsModule,
ButtonModule,
FormModule,
ButtonModule,
ButtonGroupModule,
ChartjsModule,
AvatarModule,
TableModule,
ModalModule,
DropdownModule,
SharedModule,
ListGroupModule,
BadgeModule,
TooltipModule,
FormsModule,
ToastModule,
CoreUIGridModule,
MatMenuModule,
HighlightJsModule,
ClipboardModule
],
declarations: [VpnComponent]
})
export class VpnModule {
}