mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2026-05-08 12:59:39 +00:00
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:
parent
539e8e95fe
commit
e95304af3e
22 changed files with 5070 additions and 731 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
166
src/app/providers/mikrowizard/vpn.service.ts
Normal file
166
src/app/providers/mikrowizard/vpn.service.ts
Normal 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))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
21
src/app/views/sequences/sequences-routing.module.ts
Normal file
21
src/app/views/sequences/sequences-routing.module.ts
Normal 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 {
|
||||
}
|
||||
590
src/app/views/sequences/sequences.component.html
Normal file
590
src/app/views/sequences/sequences.component.html
Normal 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}}' →
|
||||
<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">
|
||||
{{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">
|
||||
{{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>
|
||||
257
src/app/views/sequences/sequences.component.scss
Normal file
257
src/app/views/sequences/sequences.component.scss
Normal 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;
|
||||
}
|
||||
429
src/app/views/sequences/sequences.component.ts
Normal file
429
src/app/views/sequences/sequences.component.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
50
src/app/views/sequences/sequences.module.ts
Normal file
50
src/app/views/sequences/sequences.module.ts
Normal 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 { }
|
||||
|
|
@ -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 { }
|
||||
}
|
||||
|
|
|
|||
21
src/app/views/syslog-regex/syslog-regex-routing.module.ts
Normal file
21
src/app/views/syslog-regex/syslog-regex-routing.module.ts
Normal 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 {
|
||||
}
|
||||
537
src/app/views/syslog-regex/syslog-regex.component.html
Normal file
537
src/app/views/syslog-regex/syslog-regex.component.html
Normal 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<comment>...)</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>
|
||||
114
src/app/views/syslog-regex/syslog-regex.component.scss
Normal file
114
src/app/views/syslog-regex/syslog-regex.component.scss
Normal 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;
|
||||
}
|
||||
584
src/app/views/syslog-regex/syslog-regex.component.ts
Normal file
584
src/app/views/syslog-regex/syslog-regex.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/app/views/syslog-regex/syslog-regex.module.ts
Normal file
50
src/app/views/syslog-regex/syslog-regex.module.ts
Normal 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 { }
|
||||
|
|
@ -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>
|
||||
{{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>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/app/views/vpn/vpn-routing.module.ts
Normal file
21
src/app/views/vpn/vpn-routing.module.ts
Normal 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 {
|
||||
}
|
||||
621
src/app/views/vpn/vpn.component.html
Normal file
621
src/app/views/vpn/vpn.component.html
Normal 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>
|
||||
53
src/app/views/vpn/vpn.component.scss
Normal file
53
src/app/views/vpn/vpn.component.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
593
src/app/views/vpn/vpn.component.ts
Normal file
593
src/app/views/vpn/vpn.component.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
71
src/app/views/vpn/vpn.module.ts
Normal file
71
src/app/views/vpn/vpn.module.ts
Normal 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 {
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue