mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2026-07-03 16:21:33 +00:00
Compare commits
7 commits
539e8e95fe
...
f55bd4bcd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f55bd4bcd8 | ||
|
|
d38af999a5 | ||
|
|
df1223b9e2 | ||
|
|
7e3469a5da | ||
|
|
57badfe347 | ||
|
|
9298dd80b6 | ||
|
|
e95304af3e |
94 changed files with 10072 additions and 4216 deletions
|
|
@ -61,7 +61,7 @@
|
|||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "6kb"
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
|
|
|||
37
package.json
37
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "MikroWizard",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"copyright": "MikroWizard mikrowizard.com",
|
||||
"license": "AGPL",
|
||||
"author": "MikroWizard Team (https://github.com/MikroWizard)",
|
||||
|
|
@ -19,17 +19,17 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.5",
|
||||
"@angular/animations": "^18.2.14",
|
||||
"@angular/cdk": "^16.2.9",
|
||||
"@angular/common": "^17.3.5",
|
||||
"@angular/compiler": "^17.3.5",
|
||||
"@angular/core": "^17.3.5",
|
||||
"@angular/forms": "^17.3.5",
|
||||
"@angular/language-service": "^17.3.5",
|
||||
"@angular/common": "^18.2.14",
|
||||
"@angular/compiler": "^18.2.14",
|
||||
"@angular/core": "^18.2.14",
|
||||
"@angular/forms": "^18.2.14",
|
||||
"@angular/language-service": "^18.2.14",
|
||||
"@angular/material": "^17.3.5",
|
||||
"@angular/platform-browser": "^17.3.5",
|
||||
"@angular/platform-browser-dynamic": "^17.3.5",
|
||||
"@angular/router": "^17.3.5",
|
||||
"@angular/platform-browser": "^18.2.14",
|
||||
"@angular/platform-browser-dynamic": "^18.2.14",
|
||||
"@angular/router": "^18.2.14",
|
||||
"@coreui/angular": "~4.5.27",
|
||||
"@coreui/angular-chartjs": "~4.5.27",
|
||||
"@coreui/chartjs": "^3.1.2",
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
"@generic-ui/fabric": "^0.19.0",
|
||||
"@generic-ui/hermes": "^0.19.0",
|
||||
"@generic-ui/ngx-grid": "^0.19.0",
|
||||
"@primeuix/themes": "^2.0.3",
|
||||
"chart.js": "^3.9.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-jalali": "^3.6.0-0",
|
||||
|
|
@ -61,17 +62,21 @@
|
|||
"ngx-material-date-fns-adapter": "^18.0.0",
|
||||
"ngx-scrollbar": "^13.0.3",
|
||||
"ngx-super-select": "^3.17.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primeng": "^18.0.2",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.3.0",
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"zone.js": "~0.14.4"
|
||||
"vis-util": "^6.0.0",
|
||||
"zone.js": "~0.14.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.5",
|
||||
"@angular/cli": "^17.3.5",
|
||||
"@angular/compiler-cli": "^17.3.5",
|
||||
"@angular/localize": "^17.3.5",
|
||||
"@angular-devkit/build-angular": "^18.2.21",
|
||||
"@angular/cli": "^18.2.21",
|
||||
"@angular/compiler-cli": "^18.2.14",
|
||||
"@angular/localize": "^18.2.14",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
"@types/jasmine": "^5.1.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/node": "^18.19.34",
|
||||
|
|
@ -87,4 +92,4 @@
|
|||
"node": "^16.14.0 || ^18.10.0",
|
||||
"npm": ">= 6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ import { NgModule ,APP_INITIALIZER} from '@angular/core';
|
|||
import { HashLocationStrategy, LocationStrategy, PathLocationStrategy } from '@angular/common';
|
||||
import { BrowserModule, Title } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { providePrimeNG } from 'primeng/config';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import { ReactiveFormsModule,FormsModule } from '@angular/forms';
|
||||
|
||||
import { NgScrollbarModule } from 'ngx-scrollbar';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { LicenseService } from './providers/license.service';
|
||||
import { LicenseInterceptor } from './providers/license-interceptor.service';
|
||||
|
||||
// Import routing module
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
|
@ -54,60 +59,69 @@ const APP_CONTAINERS = [
|
|||
export function loginStatusProviderFactory(provider: loginChecker) {
|
||||
return () => provider.load();
|
||||
}
|
||||
@NgModule({
|
||||
declarations: [AppComponent, ...APP_CONTAINERS],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
AvatarModule,
|
||||
BreadcrumbModule,
|
||||
FooterModule,
|
||||
DropdownModule,
|
||||
GridModule,
|
||||
HeaderModule,
|
||||
SidebarModule,
|
||||
IconModule,
|
||||
NavModule,
|
||||
HttpClientModule,
|
||||
ButtonModule,
|
||||
FormModule,
|
||||
UtilitiesModule,
|
||||
ButtonGroupModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
SidebarModule,
|
||||
SharedModule,
|
||||
TabsModule,
|
||||
ListGroupModule,
|
||||
ProgressModule,
|
||||
BadgeModule,
|
||||
ListGroupModule,
|
||||
CardModule,
|
||||
NgScrollbarModule,
|
||||
ModalModule,
|
||||
FontAwesomeModule,
|
||||
TableModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: LocationStrategy,
|
||||
useClass: HashLocationStrategy
|
||||
},
|
||||
MikroWizardProvider,
|
||||
dataProvider,
|
||||
loginChecker,
|
||||
IconSetService,
|
||||
provideDateFnsAdapter(),
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: loginStatusProviderFactory,
|
||||
deps: [loginChecker],
|
||||
multi: true,
|
||||
},
|
||||
Title
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
@NgModule({ declarations: [AppComponent, ...APP_CONTAINERS],
|
||||
bootstrap: [AppComponent], imports: [BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
AvatarModule,
|
||||
BreadcrumbModule,
|
||||
FooterModule,
|
||||
DropdownModule,
|
||||
GridModule,
|
||||
HeaderModule,
|
||||
SidebarModule,
|
||||
IconModule,
|
||||
NavModule,
|
||||
ButtonModule,
|
||||
FormModule,
|
||||
UtilitiesModule,
|
||||
ButtonGroupModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
SidebarModule,
|
||||
SharedModule,
|
||||
TabsModule,
|
||||
ListGroupModule,
|
||||
ProgressModule,
|
||||
BadgeModule,
|
||||
ListGroupModule,
|
||||
CardModule,
|
||||
NgScrollbarModule,
|
||||
ModalModule,
|
||||
FontAwesomeModule,
|
||||
TableModule], providers: [
|
||||
{
|
||||
provide: LocationStrategy,
|
||||
useClass: HashLocationStrategy
|
||||
},
|
||||
MikroWizardProvider,
|
||||
dataProvider,
|
||||
loginChecker,
|
||||
IconSetService,
|
||||
provideDateFnsAdapter(),
|
||||
provideAnimationsAsync(),
|
||||
providePrimeNG({
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
darkModeSelector: 'none'
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: loginStatusProviderFactory,
|
||||
deps: [loginChecker],
|
||||
multi: true,
|
||||
},
|
||||
Title,
|
||||
LicenseService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: LicenseInterceptor,
|
||||
multi: true
|
||||
},
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
] })
|
||||
export class AppModule {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,24 @@ export const navItems: INavData[] = [
|
|||
name: 'Dashboard',
|
||||
url: '/dashboard',
|
||||
iconComponent: { name: 'cil-speedometer' },
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
name: 'Monitoring Wall',
|
||||
url: '/monitoring',
|
||||
icon:'fa-solid fa-tv',
|
||||
attributes: { 'pro':true }
|
||||
icon: 'fa-solid fa-tv',
|
||||
attributes: { 'pro': true }
|
||||
},
|
||||
{
|
||||
title: true,
|
||||
name: 'Device Managment'
|
||||
},
|
||||
{
|
||||
name: 'WireGuard Server',
|
||||
url: '/vpn',
|
||||
icon: 'fa-solid fa-network-wired',
|
||||
attributes: { 'pro': true }
|
||||
},
|
||||
{
|
||||
name: 'Devices',
|
||||
url: '/devices',
|
||||
|
|
@ -31,8 +37,8 @@ export const navItems: INavData[] = [
|
|||
{
|
||||
name: 'Network Maps',
|
||||
url: '/maps',
|
||||
icon:'fa-solid fa-map',
|
||||
attributes: { 'pro':true }
|
||||
icon: 'fa-solid fa-map',
|
||||
attributes: { 'pro': true }
|
||||
},
|
||||
// {
|
||||
// name: 'Tools',
|
||||
|
|
@ -70,18 +76,24 @@ export const navItems: INavData[] = [
|
|||
url: '/snippets',
|
||||
icon: 'fa-solid fa-code'
|
||||
},
|
||||
{
|
||||
name: 'Sequences',
|
||||
url: '/sequences',
|
||||
icon: 'fa-solid fa-code-branch',
|
||||
attributes: { 'pro': true }
|
||||
},
|
||||
{
|
||||
name: 'Sync and Cloner',
|
||||
url: '/cloner',
|
||||
icon: 'fa-solid fa-rotate',
|
||||
attributes: { 'pro':true }
|
||||
attributes: { 'pro': true }
|
||||
|
||||
},
|
||||
{
|
||||
name: 'Password Vault',
|
||||
url: '/vault',
|
||||
icon:'fa-solid fa-vault',
|
||||
attributes: { 'pro':true }
|
||||
icon: 'fa-solid fa-vault',
|
||||
attributes: { 'pro': true }
|
||||
},
|
||||
// {
|
||||
// name: 'Tools',
|
||||
|
|
@ -133,6 +145,12 @@ export const navItems: INavData[] = [
|
|||
icon: 'fa-solid fa-person-circle-question',
|
||||
|
||||
},
|
||||
{
|
||||
name: 'Syslog Custom Regex',
|
||||
url: '/syslog-regex',
|
||||
icon: 'fa-solid fa-code-commit',
|
||||
attributes: { 'pro': true }
|
||||
},
|
||||
{
|
||||
title: true,
|
||||
name: 'Users'
|
||||
|
|
@ -140,12 +158,12 @@ export const navItems: INavData[] = [
|
|||
{
|
||||
name: 'Users Management',
|
||||
url: '/user_manager',
|
||||
icon: 'fa-solid fa-user-gear' ,
|
||||
icon: 'fa-solid fa-user-gear',
|
||||
},
|
||||
{
|
||||
name: 'Permissions',
|
||||
url: '/permissions',
|
||||
icon: 'fa-solid fa-users' ,
|
||||
icon: 'fa-solid fa-users',
|
||||
},
|
||||
{
|
||||
title: true,
|
||||
|
|
@ -155,7 +173,7 @@ export const navItems: INavData[] = [
|
|||
{
|
||||
name: 'Settings',
|
||||
url: '/settings',
|
||||
icon: 'fa-solid fa-gear' ,
|
||||
icon: 'fa-solid fa-gear',
|
||||
},
|
||||
// {
|
||||
// name: 'Backup',
|
||||
|
|
@ -177,7 +195,7 @@ export const navItems: INavData[] = [
|
|||
{
|
||||
name: 'Buy Pro',
|
||||
url: 'https://mikrowizard.com/pricing/',
|
||||
icon:'fa-solid fa-money-check-dollar',
|
||||
attributes: { 'free':true,target: '_blank' }
|
||||
icon: 'fa-solid fa-money-check-dollar',
|
||||
attributes: { 'free': true, target: '_blank' }
|
||||
}
|
||||
];
|
||||
|
|
|
|||
55
src/app/providers/license-interceptor.service.ts
Normal file
55
src/app/providers/license-interceptor.service.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpHandler,
|
||||
HttpEvent,
|
||||
HttpInterceptor,
|
||||
HttpResponse
|
||||
} from '@angular/common/http';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { LicenseService } from './license.service';
|
||||
|
||||
@Injectable()
|
||||
export class LicenseInterceptor implements HttpInterceptor {
|
||||
|
||||
// Pro-licensed API endpoints that should be monitored
|
||||
private readonly proEndpoints = [
|
||||
'monitoring/devs/get',
|
||||
'monitoring/events/get',
|
||||
'monitoring/eventunfixed/get',
|
||||
'/api/vpn/status',
|
||||
'networkmap/get',
|
||||
'snippet/sequence/list',
|
||||
'cloner/list',
|
||||
'/api/pssvault/get',
|
||||
'snippet/syslogregex/list'
|
||||
];
|
||||
|
||||
constructor(private licenseService: LicenseService) { }
|
||||
|
||||
private isProEndpoint(url: string): boolean {
|
||||
return this.proEndpoints.some(endpoint => url.includes(endpoint));
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
return next.handle(request).pipe(
|
||||
tap((event: HttpEvent<any>) => {
|
||||
if (event instanceof HttpResponse && this.isProEndpoint(request.url)) {
|
||||
const body = event.body;
|
||||
if (
|
||||
body &&
|
||||
body.result &&
|
||||
typeof body.result === 'object' &&
|
||||
body.result.err === 'License Expired' &&
|
||||
body.result.status === 'failed'
|
||||
) {
|
||||
this.licenseService.setExpired(true);
|
||||
} else {
|
||||
// Pro endpoint responded without license error — license is valid
|
||||
this.licenseService.setExpired(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
22
src/app/providers/license.service.ts
Normal file
22
src/app/providers/license.service.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LicenseService {
|
||||
private isExpiredSubject = new BehaviorSubject<boolean>(false);
|
||||
public isExpired$: Observable<boolean> = this.isExpiredSubject.asObservable();
|
||||
|
||||
constructor() { }
|
||||
|
||||
setExpired(expired: boolean): void {
|
||||
if (this.isExpiredSubject.value !== expired) {
|
||||
this.isExpiredSubject.next(expired);
|
||||
}
|
||||
}
|
||||
|
||||
isExpired(): boolean {
|
||||
return this.isExpiredSubject.value;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { User } from './user';
|
|||
|
||||
@Injectable()
|
||||
export class dataProvider {
|
||||
|
||||
|
||||
// public serverUrl: string = "/api";
|
||||
public serverUrl: string = "";
|
||||
private db: string = "NothingImportant";
|
||||
|
|
@ -60,60 +60,60 @@ export class dataProvider {
|
|||
////
|
||||
//// MikroWizard API
|
||||
////
|
||||
get_front_version(){
|
||||
get_front_version() {
|
||||
return this.MikroWizardRPC.sendHttpGetRequest("/api/frontver/");
|
||||
}
|
||||
change_password(oldpass:string,newpass:string){
|
||||
var data={
|
||||
'oldpass':oldpass,
|
||||
'newpass':newpass
|
||||
change_password(oldpass: string, newpass: string) {
|
||||
var data = {
|
||||
'oldpass': oldpass,
|
||||
'newpass': newpass
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data);
|
||||
}
|
||||
dashboard_stats(versioncheck:boolean,front_version:string){
|
||||
var data={
|
||||
'versioncheck':versioncheck,
|
||||
'front_version':front_version
|
||||
dashboard_stats(versioncheck: boolean, front_version: string) {
|
||||
var data = {
|
||||
'versioncheck': versioncheck,
|
||||
'front_version': front_version
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data);
|
||||
}
|
||||
monitoring_devices_events(page:number,textfilter:string=''){
|
||||
var data={
|
||||
'page':page,
|
||||
'textfilter':textfilter
|
||||
monitoring_devices_events(page: number, textfilter: string = '') {
|
||||
var data = {
|
||||
'page': page,
|
||||
'textfilter': textfilter
|
||||
}
|
||||
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/devs/get", data);
|
||||
}
|
||||
|
||||
monitoring_events_fix(event_id:number){
|
||||
var data={
|
||||
'event_id':event_id
|
||||
|
||||
monitoring_events_fix(event_id: number) {
|
||||
var data = {
|
||||
'event_id': event_id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/fix", data);
|
||||
}
|
||||
|
||||
monitoring_all_events(devid:number,page:number){
|
||||
var data={
|
||||
'devid':devid,
|
||||
'page':page
|
||||
|
||||
monitoring_all_events(devid: number, page: number) {
|
||||
var data = {
|
||||
'devid': devid,
|
||||
'page': page
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/get", data);
|
||||
}
|
||||
monitoring_unfixed_events(devid:number){
|
||||
var data={
|
||||
'devid':devid
|
||||
monitoring_unfixed_events(devid: number) {
|
||||
var data = {
|
||||
'devid': devid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/eventunfixed/get", data);
|
||||
}
|
||||
dashboard_traffic(delta:string){
|
||||
var data={
|
||||
'delta':delta
|
||||
dashboard_traffic(delta: string) {
|
||||
var data = {
|
||||
'delta': delta
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/traffic", data);
|
||||
}
|
||||
|
||||
get_dev_list(data:any) {
|
||||
get_dev_list(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/list", data);
|
||||
}
|
||||
|
||||
|
|
@ -121,442 +121,502 @@ export class dataProvider {
|
|||
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/list", {});
|
||||
}
|
||||
|
||||
get_devgroup_members(gid:number) {
|
||||
var data={
|
||||
'gid':gid
|
||||
get_devgroup_members(gid: number) {
|
||||
var data = {
|
||||
'gid': gid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/members", data);
|
||||
}
|
||||
delete_group(id:number){
|
||||
var data={
|
||||
'gid':id
|
||||
delete_group(id: number) {
|
||||
var data = {
|
||||
'gid': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/delete", data);
|
||||
}
|
||||
|
||||
delete_devices(devids:any){
|
||||
delete_devices(devids: any) {
|
||||
var data = {
|
||||
'devids':devids
|
||||
'devids': devids
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/delete", data);
|
||||
}
|
||||
|
||||
get_dev_info(id: number) {
|
||||
var data={
|
||||
'devid':id
|
||||
var data = {
|
||||
'devid': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/info", data);
|
||||
}
|
||||
|
||||
get_editform(id: number) {
|
||||
var data={
|
||||
'devid':id
|
||||
var data = {
|
||||
'devid': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/get_editform", data);
|
||||
}
|
||||
save_editform(data:any){
|
||||
save_editform(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/save_editform", data);
|
||||
}
|
||||
get_dev_sensors(id: number,delta:string="5m",total_type:string="bps") {
|
||||
var data={
|
||||
'devid':id,
|
||||
'delta':delta,
|
||||
'total':total_type
|
||||
get_dev_sensors(id: number, delta: string = "5m", total_type: string = "bps") {
|
||||
var data = {
|
||||
'devid': id,
|
||||
'delta': delta,
|
||||
'total': total_type
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/sensors", data);
|
||||
}
|
||||
get_dev_radio_sensors(id: number, delta:string="5m"){
|
||||
var data={
|
||||
'devid':id,
|
||||
'delta':delta
|
||||
get_dev_radio_sensors(id: number, delta: string = "5m") {
|
||||
var data = {
|
||||
'devid': id,
|
||||
'delta': delta
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data);
|
||||
}
|
||||
get_dev_dhcp_info(id: number){
|
||||
var data={
|
||||
'devid':id,
|
||||
get_dev_dhcp_info(id: number) {
|
||||
var data = {
|
||||
'devid': id,
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data);
|
||||
}
|
||||
get_dev_ifstat(id: number,delta:string="5m",iface:string="ether1",type:string="bps") {
|
||||
var data={
|
||||
'devid':id,
|
||||
'delta':delta,
|
||||
'type':type,
|
||||
'interface':iface
|
||||
get_dev_ifstat(id: number, delta: string = "5m", iface: string = "ether1", type: string = "bps") {
|
||||
var data = {
|
||||
'devid': id,
|
||||
'delta': delta,
|
||||
'type': type,
|
||||
'interface': iface
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/ifstat", data);
|
||||
}
|
||||
totp(action:string,userid:string){
|
||||
var data={
|
||||
'userid':userid,
|
||||
'action':action
|
||||
totp(action: string, userid: string) {
|
||||
var data = {
|
||||
'userid': userid,
|
||||
'action': action
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/totp", data);
|
||||
}
|
||||
|
||||
get_user_restrictions(uid:string){
|
||||
var data={
|
||||
'uid':uid
|
||||
|
||||
get_user_restrictions(uid: string) {
|
||||
var data = {
|
||||
'uid': uid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/restrictions", data);
|
||||
}
|
||||
save_user_restrictions(uid:string,restrictions:any){
|
||||
var data={
|
||||
'uid':uid,
|
||||
'restrictions':restrictions
|
||||
save_user_restrictions(uid: string, restrictions: any) {
|
||||
var data = {
|
||||
'uid': uid,
|
||||
'restrictions': restrictions
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/save_restrictions", data);
|
||||
}
|
||||
|
||||
mytotp(action:string,otp:any=false){
|
||||
var data={
|
||||
'action':action,
|
||||
'otp':otp
|
||||
mytotp(action: string, otp: any = false) {
|
||||
var data = {
|
||||
'action': action,
|
||||
'otp': otp
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/mytotp", data);
|
||||
}
|
||||
|
||||
get_auth_logs(filters:any) {
|
||||
var data=filters;
|
||||
get_auth_logs(filters: any) {
|
||||
var data = filters;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/auth/list", data);
|
||||
}
|
||||
|
||||
get_account_logs(filters:any) {
|
||||
var data=filters;
|
||||
get_account_logs(filters: any) {
|
||||
var data = filters;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/account/list", data);
|
||||
}
|
||||
|
||||
get_dev_logs(filters:any) {
|
||||
var data=filters;
|
||||
get_dev_logs(filters: any) {
|
||||
var data = filters;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/list", data);
|
||||
}
|
||||
|
||||
get_syslog(filters:any) {
|
||||
var data=filters;
|
||||
get_syslog(filters: any) {
|
||||
var data = filters;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/syslog/list", data);
|
||||
}
|
||||
get_details_grouped(devid:number=0){
|
||||
var data={
|
||||
'devid':devid
|
||||
get_details_grouped(devid: number = 0) {
|
||||
var data = {
|
||||
'devid': devid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/details/list", data);
|
||||
}
|
||||
|
||||
scan_devs(type:string,info:any){
|
||||
var data: any={
|
||||
'type':type
|
||||
scan_devs(type: string, info: any) {
|
||||
var data: any = {
|
||||
'type': type
|
||||
}
|
||||
if(type=="ip"){
|
||||
if (type == "ip") {
|
||||
data = Object.assign(data, info);
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/scanner/scan", data);
|
||||
}
|
||||
|
||||
scan_results(){
|
||||
scan_results() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/scanner/results", {});
|
||||
}
|
||||
|
||||
get_groups(searchstr:string=""){
|
||||
var data={
|
||||
'searchstr':searchstr
|
||||
get_groups(searchstr: string = "") {
|
||||
var data = {
|
||||
'searchstr': searchstr
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/search/groups", data);
|
||||
}
|
||||
}
|
||||
|
||||
get_devices(searchstr:string=""){
|
||||
var data={
|
||||
'searchstr':searchstr
|
||||
get_devices(searchstr: string = "") {
|
||||
var data = {
|
||||
'searchstr': searchstr
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/search/devices", data);
|
||||
}
|
||||
}
|
||||
|
||||
update_save_group(group:any){
|
||||
var data={
|
||||
update_save_group(group: any) {
|
||||
var data = {
|
||||
...group
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/update_save_group", data);
|
||||
}
|
||||
|
||||
get_snippets(name:string,desc:string,content:string,page:number=0,size:number=1000,limit:any=false){
|
||||
var data={
|
||||
'name':name,
|
||||
'description':desc,
|
||||
'content':content,
|
||||
'page':page,
|
||||
'size':size,
|
||||
'limit':limit
|
||||
get_snippets(name: string, desc: string, content: string, page: number = 0, size: number = 1000, limit: any = false) {
|
||||
var data = {
|
||||
'name': name,
|
||||
'description': desc,
|
||||
'content': content,
|
||||
'page': page,
|
||||
'size': size,
|
||||
'limit': limit
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/list", data);
|
||||
}
|
||||
|
||||
save_snippet(data:any){
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", {...data});
|
||||
save_snippet(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", { ...data });
|
||||
}
|
||||
|
||||
Exec_snipet(data:any,members:any) {
|
||||
data['members']=members;
|
||||
Exec_snipet(data: any, members: any) {
|
||||
data['members'] = members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/exec", data);
|
||||
}
|
||||
|
||||
delete_snippet(id:number){
|
||||
var data={
|
||||
'id':id
|
||||
delete_snippet(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/delete", data);
|
||||
}
|
||||
|
||||
get_executed_snipet(id:number){
|
||||
var data={
|
||||
'id':id
|
||||
get_executed_snipet(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/executed", data);
|
||||
}
|
||||
|
||||
get_sequences() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/list", {});
|
||||
}
|
||||
|
||||
save_sequence(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/save", { ...data });
|
||||
}
|
||||
|
||||
delete_sequence(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/delete", data);
|
||||
}
|
||||
|
||||
get_sequence_history(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/history", data);
|
||||
}
|
||||
|
||||
exec_sequence(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/exec", data);
|
||||
}
|
||||
|
||||
get_syslog_regexes() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/list", {});
|
||||
}
|
||||
|
||||
save_syslog_regex(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/save", { ...data });
|
||||
}
|
||||
|
||||
delete_syslog_regex(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/delete", data);
|
||||
}
|
||||
|
||||
get_syslogregex_samples() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/samples", {});
|
||||
}
|
||||
|
||||
get_alerts() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/list", {});
|
||||
}
|
||||
|
||||
save_alert(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/save", { ...data });
|
||||
}
|
||||
|
||||
delete_alert(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/delete", data);
|
||||
}
|
||||
|
||||
get_user_task_list() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/list", {});
|
||||
}
|
||||
|
||||
Add_task(data:any,members:any) {
|
||||
data['members']=members;
|
||||
Add_task(data: any, members: any) {
|
||||
data['members'] = members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/create", data);
|
||||
}
|
||||
|
||||
Delete_task(taskid:Number) {
|
||||
var data={
|
||||
'taskid':taskid,
|
||||
Delete_task(taskid: Number) {
|
||||
var data = {
|
||||
'taskid': taskid,
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/delete", data);
|
||||
}
|
||||
|
||||
Edit_task(data:any,members:any) {
|
||||
data['members']=members;
|
||||
Edit_task(data: any, members: any) {
|
||||
data['members'] = members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/edit", data);
|
||||
}
|
||||
|
||||
get_task_members(taskid:Number) {
|
||||
var data={
|
||||
'taskid':taskid,
|
||||
get_task_members(taskid: Number) {
|
||||
var data = {
|
||||
'taskid': taskid,
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/taskmember/details", data);
|
||||
}
|
||||
|
||||
get_users(page:Number,size:Number,search:string) {
|
||||
var data={
|
||||
'page':page,
|
||||
'size':size,
|
||||
'search':search
|
||||
get_users(page: Number, size: Number, search: string) {
|
||||
var data = {
|
||||
'page': page,
|
||||
'size': size,
|
||||
'search': search
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/users/list", data);
|
||||
}
|
||||
|
||||
get_perms(page:Number,size:Number,search:string) {
|
||||
var data={
|
||||
'page':page,
|
||||
'size':size,
|
||||
'search':search
|
||||
get_perms(page: Number, size: Number, search: string) {
|
||||
var data = {
|
||||
'page': page,
|
||||
'size': size,
|
||||
'search': search
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/perms/list", data);
|
||||
}
|
||||
|
||||
create_perm(name:string,perms:any) {
|
||||
var data={
|
||||
'name':name,
|
||||
'perms':perms
|
||||
|
||||
create_perm(name: string, perms: any) {
|
||||
var data = {
|
||||
'name': name,
|
||||
'perms': perms
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/perms/create", data);
|
||||
}
|
||||
|
||||
edit_perm(id:Number,name:string,perms:any) {
|
||||
|
||||
edit_perm(id: Number, name: string, perms: any) {
|
||||
|
||||
var data = {
|
||||
'id':id,
|
||||
'name':name,
|
||||
'perms':perms
|
||||
'id': id,
|
||||
'name': name,
|
||||
'perms': perms
|
||||
}
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/perms/edit", data);
|
||||
}
|
||||
|
||||
delete_perm(id:number){
|
||||
var data={
|
||||
'id':id
|
||||
|
||||
delete_perm(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/perms/delete", data);
|
||||
}
|
||||
|
||||
get_vault_setting(){
|
||||
get_vault_setting() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get", {});
|
||||
}
|
||||
|
||||
vault_task(data:any){
|
||||
vault_task(data: any) {
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/task", data);
|
||||
}
|
||||
vault_history(){
|
||||
vault_history() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/history", {});
|
||||
}
|
||||
exec_vault(){
|
||||
exec_vault() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/execute", {});
|
||||
}
|
||||
reveal_password(devid:number,username:string){
|
||||
var data={
|
||||
'devid':devid,
|
||||
'username':username
|
||||
reveal_password(devid: number, username: string) {
|
||||
var data = {
|
||||
'devid': devid,
|
||||
'username': username
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/reveal", data);
|
||||
}
|
||||
|
||||
get_passwords(data:any){
|
||||
get_passwords(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_passwords", data);
|
||||
}
|
||||
get_device_pass(devid:number){
|
||||
var data={
|
||||
'devid':devid
|
||||
get_device_pass(devid: number) {
|
||||
var data = {
|
||||
'devid': devid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_device_pass", data);
|
||||
}
|
||||
user_perms(uid:string) {
|
||||
|
||||
user_perms(uid: string) {
|
||||
|
||||
var data = {
|
||||
'uid':uid,
|
||||
'uid': uid,
|
||||
}
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/list", data);
|
||||
}
|
||||
|
||||
Add_user_perm(uid:Number,permid:Number,devgroupid:Number){
|
||||
Add_user_perm(uid: Number, permid: Number, devgroupid: Number) {
|
||||
|
||||
var data = {
|
||||
'uid':uid,
|
||||
'pid':permid,
|
||||
'gid':devgroupid
|
||||
'uid': uid,
|
||||
'pid': permid,
|
||||
'gid': devgroupid
|
||||
}
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/create", data);
|
||||
}
|
||||
Delete_user_perm(id:number){
|
||||
var data={
|
||||
'id':id
|
||||
Delete_user_perm(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/userperms/delete", data);
|
||||
}
|
||||
edit_user(data:any) {
|
||||
edit_user(data: any) {
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/edit", data);
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/edit", data);
|
||||
}
|
||||
|
||||
create_user(data:any) {
|
||||
create_user(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/create", data);
|
||||
}
|
||||
delete_user(id:number){
|
||||
var data={
|
||||
'uid':id
|
||||
delete_user(id: number) {
|
||||
var data = {
|
||||
'uid': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/delete", data);
|
||||
}
|
||||
check_firmware(devids:any) {
|
||||
check_firmware(devids: any) {
|
||||
var data = {
|
||||
'devids':devids
|
||||
'devids': devids
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/check_firmware_update", data);
|
||||
}
|
||||
|
||||
get_firms(page:Number,size:Number,search:any) {
|
||||
get_firms(page: Number, size: Number, search: any) {
|
||||
var data = {
|
||||
'page':page,
|
||||
'size':size,
|
||||
'search':search
|
||||
'page': page,
|
||||
'size': size,
|
||||
'search': search
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data);
|
||||
}
|
||||
|
||||
delete_firm(id:number){
|
||||
var data={
|
||||
'id':id
|
||||
delete_firm(id: number) {
|
||||
var data = {
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/delete_from_repository", data);
|
||||
}
|
||||
|
||||
get_backups(data:any) {
|
||||
|
||||
get_backups(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/backup/list", data);
|
||||
}
|
||||
|
||||
get_backup(id:number){
|
||||
|
||||
get_backup(id: number) {
|
||||
var data = {
|
||||
'id':id
|
||||
'id': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data);
|
||||
}
|
||||
restore_backup(id:number){
|
||||
restore_backup(id: number) {
|
||||
var data = {
|
||||
'backupid':id
|
||||
'backupid': id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/backup/restore", data);
|
||||
}
|
||||
|
||||
get_downloadable_firms() {
|
||||
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_downloadable_firms", {});
|
||||
}
|
||||
|
||||
download_firmware_to_repository(version:string){
|
||||
download_firmware_to_repository(version: string) {
|
||||
var data = {
|
||||
'version':version
|
||||
'version': version
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/download_firmware_to_repository", data);
|
||||
}
|
||||
|
||||
save_firmware_setting(updatebehavior:string,firmwaretoinstall:string,firmwaretoinstallv6:string){
|
||||
save_firmware_setting(updatebehavior: string, firmwaretoinstall: string, firmwaretoinstallv6: string) {
|
||||
var data = {
|
||||
'updatebehavior':updatebehavior,
|
||||
'firmwaretoinstall':firmwaretoinstall,
|
||||
'firmwaretoinstallv6':firmwaretoinstallv6
|
||||
'updatebehavior': updatebehavior,
|
||||
'firmwaretoinstall': firmwaretoinstall,
|
||||
'firmwaretoinstallv6': firmwaretoinstallv6
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware_settings", data);
|
||||
}
|
||||
|
||||
update_firmware(devids:string){
|
||||
update_firmware(devids: string) {
|
||||
var data = {
|
||||
'devids':devids
|
||||
'devids': devids
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware", data);
|
||||
}
|
||||
|
||||
upgrade_firmware(devids:string){
|
||||
upgrade_firmware(devids: string) {
|
||||
var data = {
|
||||
'devids':devids
|
||||
'devids': devids
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/upgrade_firmware", data);
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/upgrade_firmware", data);
|
||||
}
|
||||
|
||||
reboot_devices(devids:string){
|
||||
reboot_devices(devids: string) {
|
||||
var data = {
|
||||
'devids':devids
|
||||
'devids': devids
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/reboot_devices", data);
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/firmware/reboot_devices", data);
|
||||
}
|
||||
|
||||
get_settings(){
|
||||
get_settings() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/get_all", {});
|
||||
}
|
||||
|
||||
save_sys_setting(data:any){
|
||||
save_sys_setting(data: any) {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data);
|
||||
}
|
||||
|
||||
get_running_tasks(){
|
||||
|
||||
get_running_tasks() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {});
|
||||
}
|
||||
stop_task(signal:number){
|
||||
var data={
|
||||
'signal':signal
|
||||
stop_task(signal: number) {
|
||||
var data = {
|
||||
'signal': signal
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data);
|
||||
}
|
||||
apply_update(action:string){
|
||||
var data={
|
||||
'action':action
|
||||
apply_update(action: string) {
|
||||
var data = {
|
||||
'action': action
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data);
|
||||
}
|
||||
|
|
@ -566,62 +626,66 @@ export class dataProvider {
|
|||
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {});
|
||||
}
|
||||
|
||||
Add_cloner(data:any,members:any) {
|
||||
data['members']=members;
|
||||
Add_cloner(data: any, members: any) {
|
||||
data['members'] = members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data);
|
||||
}
|
||||
|
||||
Delete_cloner(clonerid:number) {
|
||||
var data={
|
||||
'clonerid':clonerid,
|
||||
Delete_cloner(clonerid: number) {
|
||||
var data = {
|
||||
'clonerid': clonerid,
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data);
|
||||
}
|
||||
|
||||
Edit_cloner(data:any,members:any) {
|
||||
data['members']=members;
|
||||
Edit_cloner(data: any, members: any) {
|
||||
data['members'] = members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data);
|
||||
}
|
||||
|
||||
get_cloner_members(clonerid:number) {
|
||||
var data={
|
||||
'clonerid':clonerid,
|
||||
get_cloner_members(clonerid: number) {
|
||||
var data = {
|
||||
'clonerid': clonerid,
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data);
|
||||
}
|
||||
killSession(devid:number,item:any){
|
||||
var data={
|
||||
'devid':devid,
|
||||
'item':item
|
||||
killSession(devid: number, item: any) {
|
||||
var data = {
|
||||
'devid': devid,
|
||||
'item': item
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data);
|
||||
}
|
||||
getDhcpHistory(item:any){
|
||||
var data={
|
||||
'item':item
|
||||
getDhcpHistory(item: any) {
|
||||
var data = {
|
||||
'item': item
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data);
|
||||
}
|
||||
|
||||
getNetworkMap(){
|
||||
getNetworkMap() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/get", {});
|
||||
}
|
||||
|
||||
bulk_add_devices(devices: any[]){
|
||||
resetNetworkMap() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/reset", {});
|
||||
}
|
||||
|
||||
bulk_add_devices(devices: any[]) {
|
||||
var data = {
|
||||
'devices': devices
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add", data);
|
||||
}
|
||||
|
||||
bulk_add_status(taskId: string){
|
||||
bulk_add_status(taskId: string) {
|
||||
var data = {
|
||||
'taskId': taskId
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add_status", data);
|
||||
}
|
||||
|
||||
group_firmware_action(groupId: number, action: string){
|
||||
group_firmware_action(groupId: number, action: string) {
|
||||
var data = {
|
||||
'groupId': groupId,
|
||||
'action': action
|
||||
|
|
@ -637,7 +701,7 @@ export class dataProvider {
|
|||
this.MikroWizardRPC.clearCookeis();
|
||||
this.MikroWizardRPC.setNewSession(context, session);
|
||||
}
|
||||
|
||||
|
||||
checkSessionExpired(error: any) {
|
||||
console.log(error);
|
||||
if ('title' in error && error.title == "session_expired")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable, Inject } from '@angular/core';
|
||||
|
||||
import { HttpClient,HttpResponse } from '@angular/common/http';
|
||||
import { HttpClient, HttpResponse } from '@angular/common/http';
|
||||
|
||||
class Cookies { // cookies doesn't work with Android default browser / Ionic
|
||||
private session_id: string = "";
|
||||
|
|
|
|||
167
src/app/providers/mikrowizard/vpn.service.ts
Normal file
167
src/app/providers/mikrowizard/vpn.service.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, interval, Subject, catchError, throwError, map, switchMap } from 'rxjs';
|
||||
|
||||
export interface VpnStats {
|
||||
last_handshake: number;
|
||||
rx_bytes: number;
|
||||
tx_bytes: number;
|
||||
rx_speed?: number;
|
||||
tx_speed?: number;
|
||||
nat_mode: string;
|
||||
enabled: boolean;
|
||||
allowed_ips: string;
|
||||
}
|
||||
|
||||
export interface VpnPeer {
|
||||
id: number;
|
||||
public_key: string;
|
||||
assigned_ip: string;
|
||||
nat_mode: string;
|
||||
split_targets: string[];
|
||||
dns_server: string | null;
|
||||
persistent_keepalive: number;
|
||||
custom_interface: string | null;
|
||||
linked_device_id: number | null;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
stats?: VpnStats; // Joined from the /status API
|
||||
is_managed?: boolean;
|
||||
status?: 'online' | 'offline' | 'unreachable';
|
||||
name?: string;
|
||||
description?: string;
|
||||
scan_status?: 'starting' | 'running' | 'completed' | 'failed' | null;
|
||||
mt_user: string | null;
|
||||
mt_pass: string | null;
|
||||
mt_port: number | null;
|
||||
}
|
||||
|
||||
export interface VpnServerConfig {
|
||||
id?: number;
|
||||
api_endpoint: string;
|
||||
api_token: string | null;
|
||||
vpn_subnet: string;
|
||||
public_server_ip: string | null;
|
||||
}
|
||||
|
||||
export interface VpnStatusResponse {
|
||||
status: 'running' | 'setup_required' | 'error' | 'failed';
|
||||
peers?: VpnPeer[];
|
||||
server_config?: VpnServerConfig;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface VpnLiveStatusResponse {
|
||||
server: {
|
||||
rx_bytes: number;
|
||||
tx_bytes: number;
|
||||
rx_speed: number;
|
||||
tx_speed: number;
|
||||
};
|
||||
peers: any[]; // The live endpoint returns an array of peer objects with full stats, same as /status
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class VpnService {
|
||||
private apiUrl = '/api/vpn';
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
// System Endpoints
|
||||
getStatus(): Observable<VpnStatusResponse> {
|
||||
return this.http.get<{ result: VpnStatusResponse }>(`${this.apiUrl}/status`).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
getLiveStatus(): Observable<VpnLiveStatusResponse> {
|
||||
return this.http.get<{ result: VpnLiveStatusResponse }>(`${this.apiUrl}/status/live`).pipe(
|
||||
map(res => res.result),
|
||||
catchError(err => throwError(() => err))
|
||||
);
|
||||
}
|
||||
|
||||
resetServerCounters(): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/server/reset-counters`, {}).pipe(
|
||||
catchError(err => throwError(() => err))
|
||||
);
|
||||
}
|
||||
|
||||
getSystemConfig(): Observable<{ status: string, config: VpnServerConfig }> {
|
||||
return this.http.get<{ result: { status: string, config: VpnServerConfig } }>(`${this.apiUrl}/system/config`).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
updateSystemConfig(config: Partial<VpnServerConfig>): Observable<any> {
|
||||
return this.http.post<{ result: any }>(`${this.apiUrl}/system/config`, config).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
flushSystem(wipe_database: boolean): Observable<any> {
|
||||
return this.http.post<{ result: any }>(`${this.apiUrl}/system/flush`, { wipe_database }).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
// Peer Endpoints
|
||||
addPeer(peerData: {
|
||||
pubkey?: string,
|
||||
custom_ip?: string,
|
||||
nat_mode: 'full' | 'split' | 'off',
|
||||
split_targets: string[],
|
||||
persistent_keepalive: number,
|
||||
custom_interface?: string,
|
||||
name?: string,
|
||||
description?: string,
|
||||
mt_user?: string | null,
|
||||
mt_pass?: string | null,
|
||||
mt_port?: number | null
|
||||
}): Observable<{ status: string, peer: VpnPeer }> {
|
||||
return this.http.post<{ result: { status: string, peer: VpnPeer } }>(`${this.apiUrl}/peers/add`, peerData).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
editPeer(peerData: {
|
||||
pubkey: string,
|
||||
custom_ip?: string,
|
||||
nat_mode?: string,
|
||||
split_targets?: string[],
|
||||
persistent_keepalive?: number,
|
||||
custom_interface?: string,
|
||||
name?: string,
|
||||
description?: string,
|
||||
mt_user?: string | null,
|
||||
mt_pass?: string | null,
|
||||
mt_port?: number | null
|
||||
}): Observable<any> {
|
||||
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/edit`, peerData).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
togglePeer(pubkey: string, enabled: boolean): Observable<any> {
|
||||
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/toggle`, { pubkey, enabled }).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
deletePeer(pubkey: string): Observable<any> {
|
||||
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/delete`, { pubkey }).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
getPeerConfig(pubkey: string): Observable<{ status: string, config: string }> {
|
||||
return this.http.post<{ result: { status: string, config: string } }>(`${this.apiUrl}/peers/config`, { pubkey }).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
getPeerMikrotikScript(pubkey: string): Observable<{ status: string, script: string }> {
|
||||
return this.http.post<{ result: { status: string, script: string } }>(`${this.apiUrl}/peers/mikrotik-script`, { pubkey }).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
scanLinkedDevice(pubkey: string): Observable<any> {
|
||||
return this.http.post<{ result: any }>(`${this.apiUrl}/peers/scan`, { pubkey }).pipe(map(r => r.result));
|
||||
}
|
||||
|
||||
getPeerQrCode(pubkey: string): Observable<Blob> {
|
||||
return this.http.post(`${this.apiUrl}/peers/qrcode`, { pubkey }, { responseType: 'blob' });
|
||||
}
|
||||
|
||||
resetPeerCounters(pubkey: string): Observable<any> {
|
||||
// pubkey must be url-encoded to safely pass base64 across URL path
|
||||
return this.http.post(`${this.apiUrl}/peer/${encodeURIComponent(pubkey)}/reset-counters`, {}).pipe(
|
||||
catchError(err => throwError(() => err))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="license-overlay" *ngIf="isExpired$ | async">
|
||||
<div class="overlay-content">
|
||||
<div class="icon-container">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
</div>
|
||||
<h2>Pro Feature: License Expired</h2>
|
||||
<p>This view is restricted to pro-licensed customers. Your license has either expired or is not valid for this
|
||||
feature.</p>
|
||||
<div class="actions">
|
||||
<a href="https://mikrowizard.com/register-new-mikrowizard/" target="_blank" class="btn-renew">
|
||||
<i class="fa-solid fa-cart-shopping"></i> Renew License
|
||||
</a>
|
||||
<button class="btn-dismiss" (click)="dismiss()">Later</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
.license-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
border-radius: inherit;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
|
||||
.overlay-content {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 2.5rem;
|
||||
border-radius: 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transform: translateY(0);
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #fde8e8 0%, #f9d2d2 100%);
|
||||
color: #e55353;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e55353;
|
||||
animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #1a1c23;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.75rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4b5563;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
width: 100%;
|
||||
|
||||
.btn-renew {
|
||||
flex: 3;
|
||||
background: linear-gradient(135deg, #e55353 0%, #c53030 100%);
|
||||
color: white;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 14px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(229, 83, 83, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(229, 83, 83, 0.5);
|
||||
filter: brightness(1.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-dismiss {
|
||||
flex: 1;
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 14px;
|
||||
font-weight: 600;
|
||||
border: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.95rem;
|
||||
|
||||
&:hover {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
0% { transform: scale(1); opacity: 0.5; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { LicenseService } from '../../../providers/license.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-license-expired-overlay',
|
||||
templateUrl: './license-expired-overlay.component.html',
|
||||
styleUrls: ['./license-expired-overlay.component.scss']
|
||||
})
|
||||
export class LicenseExpiredOverlayComponent implements OnInit {
|
||||
public isExpired$: Observable<boolean>;
|
||||
|
||||
constructor(private licenseService: LicenseService) {
|
||||
this.isExpired$ = this.licenseService.isExpired$;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
dismiss(): void {
|
||||
// Locally hide the overlay for the current component session if needed
|
||||
// But usually, it should stay until the license is fixed.
|
||||
}
|
||||
}
|
||||
16
src/app/shared/shared.module.ts
Normal file
16
src/app/shared/shared.module.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LicenseExpiredOverlayComponent } from './components/license-expired-overlay/license-expired-overlay.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
LicenseExpiredOverlayComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
exports: [
|
||||
LicenseExpiredOverlayComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}">
|
||||
<c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}">
|
||||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
|
||||
<h5>Accunting Logs
|
||||
<h5>Accounting Logs
|
||||
<a style="cursor: pointer;" (click)="reinitgrid('none','none')"><i
|
||||
*ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate"
|
||||
style="color: #74C0FC;"></i>
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
</h5>
|
||||
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
|
||||
{{devid}}</c-badge>
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time.
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
|
||||
*ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
|
||||
default. Use filters to modify the date and time.
|
||||
</c-alert>
|
||||
</c-col>
|
||||
<c-col xs [lg]="1">
|
||||
|
|
@ -73,49 +75,143 @@
|
|||
</c-col>
|
||||
</div>
|
||||
</c-row>
|
||||
<gui-grid [rowDetail]="rowDetail" [source]="source" [columnMenu]="columnMenu" [paging]="paging"
|
||||
[sorting]="sorting" [infoPanel]="infoPanel" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
|
||||
<ng-template let-value="item.index" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device IP" field="devip">
|
||||
<ng-template let-value="item.devip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Username" field="username">
|
||||
<ng-template let-value="item.username" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Action" field="action">
|
||||
<ng-template let-value="item.action" let-item="item" let-index="index">
|
||||
<div>{{value}}</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Section" field="section">
|
||||
<ng-template let-value="item.section" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Date" field="created">
|
||||
<ng-template let-value="item.created" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Message" field="message" [enabled]="false">
|
||||
<ng-template let-value="item.message" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search commands/users..."
|
||||
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['username', 'name', 'address', 'config', 'section', 'action']" selectionMode="single"
|
||||
(onRowSelect)="showLogDetails($event.data)">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>#No</span>
|
||||
<p-sortIcon field="index"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="devip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device IP</span>
|
||||
<span>
|
||||
<p-sortIcon field="devip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="devip" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="username" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>User Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="username"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="action" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Action</span>
|
||||
<span>
|
||||
<p-sortIcon field="action"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="action" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="section" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Section</span>
|
||||
<span>
|
||||
<p-sortIcon field="section"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="section" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="created" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Date</span>
|
||||
<span>
|
||||
<p-sortIcon field="created"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr [pSelectableRow]="item" style="cursor: pointer;">
|
||||
<td class="text-center">{{item.index}}</td>
|
||||
<td class="fw-bold">{{item.name}}</td>
|
||||
<td>{{item.devip}}</td>
|
||||
<td>{{item.username}}</td>
|
||||
<td>{{item.action}}</td>
|
||||
<td>{{item.section}}</td>
|
||||
<td class="small">{{item.created}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center p-4">No accounting logs found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<!-- Details Drawer -->
|
||||
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '500px'}"
|
||||
header="Accounting Log Details">
|
||||
<div *ngIf="selectedLog" class="p-3">
|
||||
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-desktop me-2 text-primary"></i>Device Info</h5>
|
||||
<div class="mb-3">
|
||||
<div class="h4 m-0 text-primary">{{selectedLog.name}}</div>
|
||||
<div class="text-muted">{{selectedLog.devip}}</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-user me-2 text-primary"></i>Session Details</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold">User Name:</div>
|
||||
<div class="col-7">{{selectedLog.username}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold">User IP:</div>
|
||||
<div class="col-7">{{selectedLog.address}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold">Section:</div>
|
||||
<div class="col-7">{{selectedLog.section}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold">Action:</div>
|
||||
<div class="col-7">{{selectedLog.action}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold">Exec time:</div>
|
||||
<div class="col-7 small">{{selectedLog.created}}</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-code me-2 text-primary"></i>Executed Config</h5>
|
||||
<div class="bg-dark text-success p-3 rounded shadow-inner"
|
||||
style="max-height: 400px; overflow-y: auto; font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace; font-size: 0.9rem; border: 1px solid #333;">
|
||||
<pre class="m-0" style="white-space: pre-wrap;">{{selectedLog.config}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</p-drawer>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
import { Component, OnInit, ViewEncapsulation,Input } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild, ViewEncapsulation, Input } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiRowDetail,
|
||||
GuiSelectedRow,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
|
||||
|
|
@ -26,10 +15,14 @@ import { formatInTimeZone } from "date-fns-tz";
|
|||
})
|
||||
export class AccComponent implements OnInit {
|
||||
@Input() component_devid: any=false;
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public tz: string;
|
||||
public filterText: string;
|
||||
public uid!: number;
|
||||
public uname!: string;
|
||||
public tz!: string;
|
||||
public filterText!: string;
|
||||
public detailsVisible: boolean = false;
|
||||
public selectedLog: any = null;
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
public reloading: boolean = false;
|
||||
public filters: any = {
|
||||
devid: false,
|
||||
|
|
@ -75,78 +68,20 @@ export class AccComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
public selected_rows: any[] = [];
|
||||
public Selectedrows: any[] = [];
|
||||
public devid: number = 0;
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
rowDetail: GuiRowDetail = {
|
||||
enabled: true,
|
||||
template: (item) => {
|
||||
return `
|
||||
<div class='log-detail' style="width: 355px;">
|
||||
<h1>${item.name}</h1>
|
||||
<small>${item.devip}</small>
|
||||
<table>
|
||||
<tr>
|
||||
<td>User Address</td>
|
||||
<td>${item.address}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User Name</td>
|
||||
<td>${item.username}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Type</td>
|
||||
<td>${item.ctype}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Section</td>
|
||||
<td>${item.section}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exec time</td>
|
||||
<td>${item.created}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="code-title">Executed Config</div>
|
||||
<code>
|
||||
${item.config}
|
||||
</code>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
showLogDetails(log: any) {
|
||||
this.selectedLog = log;
|
||||
this.detailsVisible = true;
|
||||
}
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
reinitgrid(field: string, $event: any) {
|
||||
if (field == "start") this.filters["start_time"] = $event.target.value;
|
||||
|
|
@ -170,9 +105,10 @@ export class AccComponent implements OnInit {
|
|||
this.initGridTable();
|
||||
}
|
||||
OnDestroy(): void {}
|
||||
onSelectedRows(rows: Array<GuiSelectedRow>): void {
|
||||
this.rows = rows;
|
||||
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
onSelectionChange(value: any[]) {
|
||||
this.selected_rows = value;
|
||||
this.Selectedrows = value.map(item => item.id);
|
||||
this.rows = value;
|
||||
}
|
||||
|
||||
removefilter(filter: any) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import {
|
|||
|
||||
import { AccRoutingModule } from "./acc-routing.module";
|
||||
import { AccComponent } from "./acc.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { DrawerModule } from 'primeng/drawer';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
|
||||
import { MatDatepickerModule } from "@angular/material/datepicker";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
|
|
@ -32,7 +34,9 @@ import { FormsModule } from "@angular/forms";
|
|||
ButtonModule,
|
||||
FormModule,
|
||||
ButtonModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
DrawerModule,
|
||||
InputTextModule,
|
||||
CollapseModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}">
|
||||
<c-card class="mb-4" [ngStyle]="component_devid && {'border-top': 'none'}">
|
||||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
</h5>
|
||||
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
|
||||
{{devid}}</c-badge>
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time.
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
|
||||
*ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
|
||||
default. Use filters to modify the date and time.
|
||||
</c-alert>
|
||||
</c-col>
|
||||
<c-col xs [lg]="1">
|
||||
|
|
@ -95,64 +97,180 @@
|
|||
</div>
|
||||
|
||||
</c-row>
|
||||
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[infoPanel]="infoPanel" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
|
||||
<ng-template let-value="item.index" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<i *ngIf="item.stype=='local'" cTooltip="local user"
|
||||
style="color: rgb(255, 42, 0); margin-right: 3px;font-size: .7em;" class="fa-solid fa-user-tie"></i>
|
||||
<i *ngIf="item.stype=='radius'" cTooltip="Update failed"
|
||||
style="color: rgb(9, 97, 20); margin-right: 3px;font-size: .7em;" class="fa-solid fa-server"></i>
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device IP" field="devip">
|
||||
<ng-template let-value="item.devip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Username" field="username">
|
||||
<ng-template let-value="item.username" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="With" field="by">
|
||||
<ng-template let-value="item.by" let-item="item" let-index="index">
|
||||
<div>{{value}}</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<div class="mb-3 d-flex justify-content-end gap-2">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gui-grid-column header="Time/Msg" field="duration">
|
||||
<ng-template let-value="item.duration" let-item="item" let-index="index">
|
||||
<span *ngIf="item.ltype!='failed'">{{value}}</span>
|
||||
<span *ngIf="item.ltype=='failed'">{{item.message}}</span>
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['username', 'name', 'ip', 'devip', 'by']" [(selection)]="selected_rows"
|
||||
(onRowSelect)="showLogDetails($event.data)" selectionMode="single">
|
||||
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>#No</span>
|
||||
<p-sortIcon field="index"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="devip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device IP</span>
|
||||
<span>
|
||||
<p-sortIcon field="devip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="devip" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="username" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Username</span>
|
||||
<span>
|
||||
<p-sortIcon field="username"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="by" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>With</span>
|
||||
<span>
|
||||
<p-sortIcon field="by"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="by" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="ip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>IP Address</span>
|
||||
<span>
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="duration" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Time/Msg</span>
|
||||
<span>
|
||||
<p-sortIcon field="duration"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="duration" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="ltype" style="width: 120px" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>State</span>
|
||||
<p-sortIcon field="ltype"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ltype" display="menu" class="ms-auto" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="created" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Date</span>
|
||||
<p-sortIcon field="created"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<gui-grid-column header="State" field="ltype" [width]="110">
|
||||
<ng-template let-value="item.ltype" let-item="item.id" let-index="index">
|
||||
<c-badge color="success" *ngIf="value=='loggedin'"> Logged In</c-badge>
|
||||
<c-badge color="warning" *ngIf="value=='loggedout'"> Logged Out</c-badge>
|
||||
<c-badge color="danger" *ngIf="value=='failed'"> Failed</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Date" field="created">
|
||||
<ng-template let-value="item.created" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr [pSelectableRow]="item" style="cursor: pointer;">
|
||||
<td class="text-center">{{item.index}}</td>
|
||||
<td>
|
||||
<i *ngIf="item.stype=='local'" pTooltip="local user" style="color: #ff2a00; margin-right: 3px;"
|
||||
class="fa-solid fa-user-tie"></i>
|
||||
<i *ngIf="item.stype=='radius'" pTooltip="radius user" style="color: #096114; margin-right: 3px;"
|
||||
class="fa-solid fa-server"></i>
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>{{item.devip}}</td>
|
||||
<td>{{item.username}}</td>
|
||||
<td>{{item.by}}</td>
|
||||
<td>{{item.ip}}</td>
|
||||
<td>
|
||||
<span *ngIf="item.ltype!='failed'">{{item.duration}}</span>
|
||||
<span *ngIf="item.ltype=='failed'" class="text-muted small">{{item.message}}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<c-badge color="success" *ngIf="item.ltype=='loggedin'"> Logged In</c-badge>
|
||||
<c-badge color="warning" *ngIf="item.ltype=='loggedout'"> Logged Out</c-badge>
|
||||
<c-badge color="danger" *ngIf="item.ltype=='failed'"> Failed</c-badge>
|
||||
</td>
|
||||
<td class="small">{{item.created}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center p-4">No authentication logs found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<!-- Details Drawer -->
|
||||
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '450px'}" header="Auth Log Details">
|
||||
<div *ngIf="selectedLog" class="p-3">
|
||||
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-shield me-2 text-primary"></i>Session Info</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Device:</div>
|
||||
<div class="col-8">{{selectedLog.name}} ({{selectedLog.devip}})</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">User:</div>
|
||||
<div class="col-8">{{selectedLog.username}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Method:</div>
|
||||
<div class="col-8">{{selectedLog.by}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Status:</div>
|
||||
<div class="col-8">
|
||||
<c-badge color="success" *ngIf="selectedLog.ltype=='loggedin'"> Logged In</c-badge>
|
||||
<c-badge color="warning" *ngIf="selectedLog.ltype=='loggedout'"> Logged Out</c-badge>
|
||||
<c-badge color="danger" *ngIf="selectedLog.ltype=='failed'"> Failed</c-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-map-marker me-2 text-primary"></i>Connection</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">IP/MAC:</div>
|
||||
<div class="col-8">{{selectedLog.ip}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Duration:</div>
|
||||
<div class="col-8">{{selectedLog.duration}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Date:</div>
|
||||
<div class="col-8 small">{{selectedLog.created}}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedLog.message" class="mt-4">
|
||||
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-envelope me-2 text-primary"></i>System Message</h5>
|
||||
<div class="bg-light p-2 rounded small border text-break">
|
||||
{{selectedLog.message}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-drawer>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,8 @@
|
|||
import { Component, OnInit, ViewEncapsulation,Input } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild, ViewEncapsulation, Input } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSelectedRow,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
interface IUser {
|
||||
|
|
@ -37,10 +27,14 @@ interface IUser {
|
|||
})
|
||||
export class AuthComponent implements OnInit {
|
||||
@Input() component_devid: any=false;
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid!: number;
|
||||
public uname!: string;
|
||||
public tz: string = "UTC";
|
||||
public filterText: string;
|
||||
public filterText!: string;
|
||||
public detailsVisible: boolean = false;
|
||||
public selectedLog: any = null;
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
public devid: number = 0;
|
||||
public reloading: boolean = false;
|
||||
public filters: any = {
|
||||
|
|
@ -87,42 +81,19 @@ export class AuthComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
public selected_rows: any[] = []; // Used by p-table selection
|
||||
public Selectedrows: any[] = []; // ID array for legacy actions
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
showLogDetails(log: any) {
|
||||
this.selectedLog = log;
|
||||
this.detailsVisible = true;
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
reinitgrid(field: string, $event: any) {
|
||||
if (field == "start") this.filters["start_time"] = $event.target.value;
|
||||
|
|
@ -179,9 +150,10 @@ export class AuthComponent implements OnInit {
|
|||
}
|
||||
this.initGridTable();
|
||||
}
|
||||
onSelectedRows(rows: Array<GuiSelectedRow>): void {
|
||||
this.rows = rows;
|
||||
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
onSelectionChange(value: any[]) {
|
||||
this.selected_rows = value;
|
||||
this.Selectedrows = value.map(item => item.id);
|
||||
this.rows = value;
|
||||
}
|
||||
|
||||
removefilter(filter: any) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import {
|
|||
} from '@coreui/angular';
|
||||
import { AuthRoutingModule } from './auth-routing.module';
|
||||
import { AuthComponent } from './auth.component';
|
||||
import { GuiGridModule } from '@generic-ui/ngx-grid';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { DrawerModule } from 'primeng/drawer';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
|
|
@ -27,7 +30,10 @@ import {MatSelectModule} from '@angular/material/select';
|
|||
GridModule,
|
||||
FormsModule,
|
||||
ButtonModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
DrawerModule,
|
||||
InputTextModule,
|
||||
TooltipModule,
|
||||
CollapseModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
|
|
|
|||
|
|
@ -20,15 +20,14 @@
|
|||
<i class="fa-solid fa-code-compare me-1"></i>{{compareitems.length}} Selected
|
||||
</c-badge>
|
||||
<div class="selected-items d-flex gap-1">
|
||||
<c-badge color="secondary" *ngFor="let item of compareitems; index as i"
|
||||
<c-badge color="secondary" *ngFor="let item of compareitems; index as i"
|
||||
class="selected-item d-flex align-items-center">
|
||||
<span class="me-1">{{item.devname}}</span>
|
||||
<i class="fa-solid fa-times cursor-pointer" (click)="delete_compare(i)"
|
||||
<i class="fa-solid fa-times cursor-pointer" (click)="delete_compare(i)"
|
||||
title="Remove from comparison"></i>
|
||||
</c-badge>
|
||||
</div>
|
||||
<button *ngIf="compareitems.length > 1" (click)="start_compare()"
|
||||
cButton color="success" size="sm">
|
||||
<button *ngIf="compareitems.length > 1" (click)="start_compare()" cButton color="success" size="sm">
|
||||
<i class="fa-solid fa-code-compare me-1"></i>Compare
|
||||
</button>
|
||||
<button (click)="clearAllCompare()" cButton color="secondary" size="sm" variant="outline">
|
||||
|
|
@ -39,7 +38,7 @@
|
|||
<!-- Filter Toggle -->
|
||||
<button (click)="toggleCollapse()" cButton color="primary" variant="outline">
|
||||
<i class="fa-solid fa-filter me-1"></i>Filters
|
||||
<i class="fa-solid" [class]="filters_visible ? 'fa-chevron-up' : 'fa-chevron-down'"
|
||||
<i class="fa-solid" [class]="filters_visible ? 'fa-chevron-up' : 'fa-chevron-down'"
|
||||
style="margin-left: 0.5rem; font-size: 0.8rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -84,8 +83,8 @@
|
|||
<i class="fa-solid fa-magnifying-glass me-1 text-primary"></i>Config Search
|
||||
</label>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']"
|
||||
matInput placeholder="Search in configurations...">
|
||||
<input (ngModelChange)="reinitgrid('search',$event)" [(ngModel)]="filters['search']" matInput
|
||||
placeholder="Search in configurations...">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</c-col>
|
||||
|
|
@ -93,65 +92,104 @@
|
|||
</c-card-body>
|
||||
</c-card>
|
||||
</div>
|
||||
<gui-grid [source]="source" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[infoPanel]="infoPanel" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[autoResizeWidth]=true>
|
||||
<gui-grid-column header="#No" type="NUMBER" field="index" width=25 align="CENTER">
|
||||
<ng-template let-value="item.index" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device Name" field="devname">
|
||||
<ng-template let-value="item.devname" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device IP" field="devip">
|
||||
<ng-template let-value="item.devip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="backup Time" field="createdC">
|
||||
<ng-template let-value="item.createdC" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="File Size" field="filesize">
|
||||
<ng-template let-value="item.filesize" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="MAC" field="devmac" [enabled]="false">
|
||||
<ng-template let-value="item.devmac" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" field="id" width="280">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<div class="d-flex gap-1">
|
||||
<button cButton [disabled]="backuploading" color="primary" size="sm"
|
||||
(click)="ShowBackup(item)" variant="outline" title="View backup content">
|
||||
<i *ngIf="backuploading && currentBackup?.id === item.id"
|
||||
class="fa-solid fa-spinner fa-spin me-1"></i>
|
||||
<i *ngIf="!backuploading || currentBackup?.id !== item.id"
|
||||
class="fa-solid fa-eye me-1"></i>
|
||||
View
|
||||
</button>
|
||||
<button *ngIf="ispro" cButton color="info" size="sm" variant="outline"
|
||||
(click)="add_for_compare(item)" title="Add to comparison"
|
||||
[disabled]="isInCompareList(item)">
|
||||
<i class="fa-solid fa-code-compare me-1"></i>
|
||||
{{isInCompareList(item) ? 'Added' : 'Compare'}}
|
||||
</button>
|
||||
<button *ngIf="ispro" cButton color="warning" size="sm" variant="outline"
|
||||
(click)="restore_backup(false, false, item)" title="Restore this backup">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search backups..."
|
||||
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['devname', 'devip', 'createdC', 'filesize']" [loading]="loading">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="index" style="width: 70px" class="text-center" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>#No</span>
|
||||
<p-sortIcon field="index"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="devname" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="devname"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="devname" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="devip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device IP</span>
|
||||
<span>
|
||||
<p-sortIcon field="devip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="devip" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="createdC" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Backup Time</span>
|
||||
<span>
|
||||
<p-sortIcon field="createdC"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="createdC" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="filesize" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>File Size</span>
|
||||
<span>
|
||||
<p-sortIcon field="filesize"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="filesize" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td class="text-center">{{item.index}}</td>
|
||||
<td>{{item.devname}}</td>
|
||||
<td>{{item.devip}}</td>
|
||||
<td>{{item.createdC}}</td>
|
||||
<td>{{item.filesize}}</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
<button cButton [disabled]="backuploading" color="primary" size="sm" (click)="ShowBackup(item)"
|
||||
variant="outline" title="View backup content">
|
||||
<i *ngIf="backuploading && currentBackup?.id === item.id"
|
||||
class="fa-solid fa-spinner fa-spin me-1"></i>
|
||||
<i *ngIf="!backuploading || currentBackup?.id !== item.id" class="fa-solid fa-eye me-1"></i>
|
||||
View
|
||||
</button>
|
||||
<button *ngIf="ispro" cButton color="info" size="sm" variant="outline" (click)="add_for_compare(item)"
|
||||
title="Add to comparison" [disabled]="isInCompareList(item)">
|
||||
<i class="fa-solid fa-code-compare me-1"></i>
|
||||
{{isInCompareList(item) ? 'Added' : 'Compare'}}
|
||||
</button>
|
||||
<button *ngIf="ispro" cButton color="warning" size="sm" variant="outline"
|
||||
(click)="restore_backup(false, false, item)" title="Restore this backup">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center p-4">No backups found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
@ -166,11 +204,12 @@
|
|||
<!-- <pre style="height: 100%;">
|
||||
<code *ngIf="!loading" language="yaml" style="height:70vh" [highlight]="codeForHighlightAuto"
|
||||
lineNumbers></code></pre> -->
|
||||
<div highlight-js [lang]="hlang" [options]="{}" >{{codeForHighlightAuto}}</div>
|
||||
<div highlight-js [lang]="hlang" [options]="{}">{{codeForHighlightAuto}}</div>
|
||||
|
||||
</c-modal-body>
|
||||
<c-modal-footer style="justify-content: space-between;">
|
||||
<button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null" (click)="copy_this()" cButton color="secondary">
|
||||
<button [cdkCopyToClipboard]="codeForHighlightAuto" [style.background-color]="copy_msg ? 'green' : null"
|
||||
(click)="copy_this()" cButton color="secondary">
|
||||
<i class="fa-regular fa-copy"></i> To clipboard
|
||||
</button>
|
||||
<div>
|
||||
|
|
@ -230,7 +269,7 @@
|
|||
<i class="fa-solid fa-rotate-left fa-3x text-warning mb-3"></i>
|
||||
<h6>Restore Configuration Backup</h6>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="backup-info bg-light p-3 rounded mb-3">
|
||||
<h6 class="mb-2"><i class="fa-solid fa-info-circle me-2 text-primary"></i>Backup Details</h6>
|
||||
<div class="row">
|
||||
|
|
@ -276,7 +315,7 @@
|
|||
|
||||
<div class="confirmation-box bg-light border border-danger p-3 rounded">
|
||||
<p class="mb-3 fw-bold">To proceed with this critical action, type <code>CONFIRM</code> in the box below:</p>
|
||||
<input cFormControl [(ngModel)]="confirmationText" placeholder="Type CONFIRM to proceed"
|
||||
<input cFormControl [(ngModel)]="confirmationText" placeholder="Type CONFIRM to proceed"
|
||||
class="form-control text-center fw-bold" style="letter-spacing: 2px;" />
|
||||
</div>
|
||||
|
||||
|
|
@ -290,8 +329,7 @@
|
|||
<button (click)="cancelCriticalRestore()" cButton color="secondary">
|
||||
<i class="fa-solid fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button (click)="restore_backup(true, true)" cButton color="danger"
|
||||
[disabled]="confirmationText !== 'CONFIRM'">
|
||||
<button (click)="restore_backup(true, true)" cButton color="danger" [disabled]="confirmationText !== 'CONFIRM'">
|
||||
<i class="fa-solid fa-rotate-left me-1"></i>RESTORE BACKUP
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,8 @@
|
|||
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
|
||||
import { Component, OnInit, QueryList, ViewChildren, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSearching,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
|
|
@ -22,10 +12,10 @@ import { AppToastComponent } from "../toast-simple/toast.component";
|
|||
styleUrls: ["backups.component.scss"],
|
||||
})
|
||||
export class BackupsComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public tz: string = "UTC";
|
||||
public filterText: string;
|
||||
public filterText: string = '';
|
||||
public filters: any = {};
|
||||
public codeForHighlightAuto: string = "";
|
||||
public ispro: boolean = false;
|
||||
|
|
@ -69,34 +59,19 @@ export class BackupsComponent implements OnInit {
|
|||
return value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
}
|
||||
@ViewChild("dt") table!: Table;
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public backuploading : boolean=false;
|
||||
public backuploading: boolean = false;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
public BakcupModalVisible: boolean = false;
|
||||
public devid: number = 0;
|
||||
public filters_visible: boolean = false;
|
||||
public currentBackup:any=false;
|
||||
public hlang:string='';
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
public currentBackup: any = false;
|
||||
public hlang: string = '';
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
|
|
@ -105,25 +80,10 @@ export class BackupsComponent implements OnInit {
|
|||
fade: true,
|
||||
closeButton: true,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.devid = Number(this.route.snapshot.paramMap.get("devid"));
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ import {
|
|||
|
||||
import { BackupsRoutingModule } from "./backups-routing.module";
|
||||
import { BackupsComponent } from "./backups.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { MatDatepickerModule } from "@angular/material/datepicker";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
|
|
@ -35,7 +37,9 @@ import { ClipboardModule } from "@angular/cdk/clipboard";
|
|||
FormModule,
|
||||
FormsModule,
|
||||
ButtonModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
TooltipModule,
|
||||
CollapseModule,
|
||||
BadgeModule,
|
||||
AlertModule,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<div style="position: relative;">
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4">
|
||||
|
|
@ -13,107 +14,172 @@
|
|||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[infoPanel]="infoPanel" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<strong>{{value}}</strong>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Description" field="description">
|
||||
<ng-template let-value="item.description" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Direction" field="direction">
|
||||
<ng-template let-value="item.direction" let-item="item" let-index="index">
|
||||
<c-badge [color]="value == 'twoway' ? 'success' : 'warning'">{{value == 'twoway' ? 'Two Way' : 'Master Mode'}}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Live Mode" field="live_mode">
|
||||
<ng-template let-value="item.live_mode" let-item="item" let-index="index">
|
||||
<c-badge [color]="value ? 'success' : 'secondary'">{{value ? 'Active' : 'Inactive'}}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Schedule" field="desc_cron">
|
||||
<ng-template let-value="item.desc_cron" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="warning" size="sm" (click)="editAddCloner(item,'edit');"><i
|
||||
class="fa-regular fa-pen-to-square"></i></button>
|
||||
<!-- <button cButton color="info" size="sm" (click)="confirm_run(item);" class="mx-1"><i
|
||||
class="fa-solid fa-bolt"></i></button> -->
|
||||
<button class=" mx-1" cButton color="danger" size="sm" (click)="confirm_delete(item);"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search cloner..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name', 'description', 'direction']" [loading]="loading">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="description" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Description</span>
|
||||
<span>
|
||||
<p-sortIcon field="description"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="direction" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Direction</span>
|
||||
<span>
|
||||
<p-sortIcon field="direction"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="direction" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="live_mode" class="text-center" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Live Mode</span>
|
||||
<span>
|
||||
<p-sortIcon field="live_mode"></p-sortIcon>
|
||||
<p-columnFilter type="boolean" field="live_mode" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="desc_cron" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Schedule</span>
|
||||
<span>
|
||||
<p-sortIcon field="desc_cron"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="desc_cron" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><strong>{{item.name}}</strong></td>
|
||||
<td>{{item.description}}</td>
|
||||
<td>
|
||||
<c-badge [color]="item.direction == 'twoway' ? 'success' : 'warning'">
|
||||
{{item.direction == 'twoway' ? 'Two Way' : 'Master Mode'}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<c-badge [color]="item.live_mode ? 'success' : 'secondary'">{{item.live_mode ? 'Active' :
|
||||
'Inactive'}}</c-badge>
|
||||
</td>
|
||||
<td>{{item.desc_cron}}</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
<button cButton color="warning" size="sm" (click)="editAddCloner(item,'edit');"
|
||||
pTooltip="Edit Cloner">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item);" pTooltip="Delete Cloner">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center p-4">No cloners found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
|
||||
<c-modal #EditClonerModal backdrop="static" size="lg" [(visible)]="EditClonerModalVisible" id="EditClonerModal">
|
||||
<c-modal-header class="bg-light">
|
||||
<h5 *ngIf="SelectedCloner['action']=='edit'" cModalTitle><i class="fa-solid fa-edit me-2"></i>Edit Cloner: {{SelectedCloner['name']}}</h5>
|
||||
<h5 *ngIf="SelectedCloner['action']=='add'" cModalTitle><i class="fa-solid fa-plus me-2"></i>Add New Cloner</h5>
|
||||
<button [cModalToggle]="EditClonerModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-3">
|
||||
<!-- Basic Information -->
|
||||
<div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
|
||||
</div>
|
||||
<c-row class="g-2">
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Cloner Name" [(ngModel)]="SelectedCloner['name']" class="form-input-sm" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Description" [(ngModel)]="SelectedCloner['description']" class="form-input-sm" />
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-modal #EditClonerModal backdrop="static" size="lg" [(visible)]="EditClonerModalVisible" id="EditClonerModal">
|
||||
<c-modal-header class="bg-light">
|
||||
<h5 *ngIf="SelectedCloner['action']=='edit'" cModalTitle><i class="fa-solid fa-edit me-2"></i>Edit Cloner:
|
||||
{{SelectedCloner['name']}}</h5>
|
||||
<h5 *ngIf="SelectedCloner['action']=='add'" cModalTitle><i class="fa-solid fa-plus me-2"></i>Add New Cloner</h5>
|
||||
<button [cModalToggle]="EditClonerModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-3">
|
||||
<!-- Basic Information -->
|
||||
<div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
|
||||
</div>
|
||||
<c-row class="g-2">
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Cloner Name" [(ngModel)]="SelectedCloner['name']" class="form-input-sm" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Description" [(ngModel)]="SelectedCloner['description']"
|
||||
class="form-input-sm" />
|
||||
</c-col>
|
||||
</c-row>
|
||||
</div>
|
||||
|
||||
<!-- Sync Configuration -->
|
||||
<div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-sync me-2"></i>Synchronization Settings</h6>
|
||||
</div>
|
||||
<c-row class="g-2">
|
||||
<c-col xs="12" md="4">
|
||||
<label class="form-label-xs">Direction</label>
|
||||
<select cSelect [(ngModel)]="SelectedCloner['direction']" (change)="onDirectionChange()" class="form-select-xs">
|
||||
<option value="twoway">Two Way Sync</option>
|
||||
<option value="oneway">Master Mode</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
|
||||
<label class="form-label-xs">Live Mode</label>
|
||||
<select cSelect [(ngModel)]="SelectedCloner['live_mode']" class="form-select-xs">
|
||||
<option [ngValue]="false">Inactive</option>
|
||||
<option [ngValue]="true">Active</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
|
||||
<label class="form-label-xs">Schedule</label>
|
||||
<select cSelect [(ngModel)]="SelectedCloner['schedule']" class="form-select-xs">
|
||||
<option [ngValue]="false">Inactive</option>
|
||||
<option [ngValue]="true">Active</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" *ngIf="SelectedCloner['schedule'] && SelectedCloner['direction']=='oneway'">
|
||||
<label class="form-label-xs">Cron Expression</label>
|
||||
<input cFormControl placeholder="0 0 * * *" [(ngModel)]="SelectedCloner['cron']" class="form-input-sm" />
|
||||
</c-col>
|
||||
</c-row>
|
||||
<!-- Sync Configuration -->
|
||||
<div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-sync me-2"></i>Synchronization Settings</h6>
|
||||
</div>
|
||||
<c-row class="g-2">
|
||||
<c-col xs="12" md="4">
|
||||
<label class="form-label-xs">Direction</label>
|
||||
<select cSelect [(ngModel)]="SelectedCloner['direction']" (change)="onDirectionChange()"
|
||||
class="form-select-xs">
|
||||
<option value="twoway">Two Way Sync</option>
|
||||
<option value="oneway">Master Mode</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
|
||||
<label class="form-label-xs">Live Mode</label>
|
||||
<select cSelect [(ngModel)]="SelectedCloner['live_mode']" class="form-select-xs">
|
||||
<option [ngValue]="false">Inactive</option>
|
||||
<option [ngValue]="true">Active</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="4" *ngIf="SelectedCloner['direction']=='oneway'">
|
||||
<label class="form-label-xs">Schedule</label>
|
||||
<select cSelect [(ngModel)]="SelectedCloner['schedule']" class="form-select-xs">
|
||||
<option [ngValue]="false">Inactive</option>
|
||||
<option [ngValue]="true">Active</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" *ngIf="SelectedCloner['schedule'] && SelectedCloner['direction']=='oneway'">
|
||||
<label class="form-label-xs">Cron Expression</label>
|
||||
<input cFormControl placeholder="0 0 * * *" [(ngModel)]="SelectedCloner['cron']" class="form-input-sm" />
|
||||
</c-col>
|
||||
</c-row>
|
||||
</div>
|
||||
|
||||
<!-- Peers Configuration (Currently disabled - only devices supported) -->
|
||||
<!-- <div class="cloner-form-section mb-3">
|
||||
<!-- Peers Configuration (Currently disabled - only devices supported) -->
|
||||
<!-- <div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-network-wired me-2"></i>Peers Configuration</h6>
|
||||
<small class="text-muted">Select the type of peers to synchronize</small>
|
||||
|
|
@ -129,233 +195,301 @@
|
|||
</c-row>
|
||||
</div> -->
|
||||
|
||||
<!-- Commands Configuration -->
|
||||
<div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-terminal me-2"></i>Commands Configuration</h6>
|
||||
</div>
|
||||
<div class="commands-container-compact">
|
||||
<div class="nav nav-underline commands-nav-compact">
|
||||
<div class="nav-item" *ngFor="let tab of tabs; let i = index">
|
||||
<a class="nav-link" [active]="i==0" [cTabContent]="tabContent" [tabPaneIdx]="i">{{ tab.name }}</a>
|
||||
</div>
|
||||
<!-- Commands Configuration -->
|
||||
<div class="cloner-form-section mb-3">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-terminal me-2"></i>Commands Configuration</h6>
|
||||
</div>
|
||||
<div class="commands-container-compact">
|
||||
<div class="nav nav-underline commands-nav-compact">
|
||||
<div class="nav-item" *ngFor="let tab of tabs; let i = index">
|
||||
<a class="nav-link" [active]="i==0" [cTabContent]="tabContent" [tabPaneIdx]="i">{{ tab.name }}</a>
|
||||
</div>
|
||||
<c-tab-content class="command-sections-compact" #tabContent="cTabContent">
|
||||
<c-tab-pane *ngFor="let tab of tabs; let i = index">
|
||||
<div class="command-category-compact" *ngFor="let section of tab.sections">
|
||||
<h6 class="category-title-compact">{{ section.title }}</h6>
|
||||
<div class="commands-grid-compact">
|
||||
<div class="command-item-compact" *ngFor="let command of section.commands">
|
||||
<div class="command-content-compact">
|
||||
<span class="command-name-compact">{{ command }}</span>
|
||||
<div class="custom-switch-compact">
|
||||
<input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)" (click)="activate_command(command)" [id]="command.replace('/', '')">
|
||||
<label class="custom-control-label" [for]="command.replace('/', '')"></label>
|
||||
</div>
|
||||
</div>
|
||||
<c-tab-content class="command-sections-compact" #tabContent="cTabContent">
|
||||
<c-tab-pane *ngFor="let tab of tabs; let i = index">
|
||||
<div class="command-category-compact" *ngFor="let section of tab.sections">
|
||||
<h6 class="category-title-compact">{{ section.title }}</h6>
|
||||
<div class="commands-grid-compact">
|
||||
<div class="command-item-compact" *ngFor="let command of section.commands">
|
||||
<div class="command-content-compact">
|
||||
<span class="command-name-compact">{{ command }}</span>
|
||||
<div class="custom-switch-compact">
|
||||
<input type="checkbox" class="custom-control-input" [checked]="in_active_commands(command)"
|
||||
(click)="activate_command(command)" [id]="command.replace('/', '')">
|
||||
<label class="custom-control-label" [for]="command.replace('/', '')"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-tab-pane>
|
||||
</c-tab-content>
|
||||
</div>
|
||||
</c-tab-pane>
|
||||
</c-tab-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Master Device Selection (for Master Mode) -->
|
||||
<div class="cloner-form-section mb-2" *ngIf="SelectedCloner['direction']=='oneway' && SelectedMembers.length > 0">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-crown me-2 text-warning"></i>Master Device</h6>
|
||||
</div>
|
||||
<div class="master-selection-compact">
|
||||
<div class="master-device-compact" *ngIf="master > 0">
|
||||
<i class="fa-solid fa-server me-2 text-primary"></i>
|
||||
<strong>{{getMasterDeviceName()}}</strong>
|
||||
<c-badge color="warning" class="ms-2">Master</c-badge>
|
||||
</div>
|
||||
<div class="no-master-compact" *ngIf="master == 0">
|
||||
<i class="fa-solid fa-exclamation-triangle me-2 text-warning"></i>
|
||||
<span class="text-muted">No master device selected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Management -->
|
||||
<div class="cloner-form-section">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-server me-2"></i>Device Management</h6>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<c-badge color="info">{{SelectedMembers.length}} device(s)</c-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Master Device Selection (for Master Mode) -->
|
||||
<div class="cloner-form-section mb-2" *ngIf="SelectedCloner['direction']=='oneway' && SelectedMembers.length > 0">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-crown me-2 text-warning"></i>Master Device</h6>
|
||||
</div>
|
||||
<div class="master-selection-compact">
|
||||
<div class="master-device-compact" *ngIf="master > 0">
|
||||
<i class="fa-solid fa-server me-2 text-primary"></i>
|
||||
<strong>{{getMasterDeviceName()}}</strong>
|
||||
<c-badge color="warning" class="ms-2">Master</c-badge>
|
||||
<div class="peers-container">
|
||||
<div *ngIf="SelectedMembers.length == 0" class="empty-peers">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-users-slash fa-2x text-muted opacity-50"></i>
|
||||
</div>
|
||||
<div class="no-master-compact" *ngIf="master == 0">
|
||||
<i class="fa-solid fa-exclamation-triangle me-2 text-warning"></i>
|
||||
<span class="text-muted">No master device selected</span>
|
||||
<div class="empty-text">
|
||||
<strong>No peers added</strong>
|
||||
<p class="mb-0 text-muted">Click "Add Members" to start adding peers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Management -->
|
||||
<div class="cloner-form-section">
|
||||
<div class="section-header mb-2">
|
||||
<h6 class="section-title mb-0"><i class="fa-solid fa-server me-2"></i>Device Management</h6>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<c-badge color="info">{{SelectedMembers.length}} device(s)</c-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="peers-container">
|
||||
<div *ngIf="SelectedMembers.length == 0" class="empty-peers">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-users-slash fa-2x text-muted opacity-50"></i>
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
<strong>No peers added</strong>
|
||||
<p class="mb-0 text-muted">Click "Add Members" to start adding peers</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="SelectedMembers.length > 0">
|
||||
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[rowSelection]="rowSelection" [autoResizeWidth]=true [paging]="paging">
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid fa-crown me-2 text-warning" *ngIf="SelectedCloner['direction']=='oneway' && item.id==master" title="Master Device"></i>
|
||||
<i class="fa-solid fa-server me-2 text-primary" *ngIf="!(SelectedCloner['direction']=='oneway' && item.id==master)"></i>
|
||||
<strong>{{value}}</strong>
|
||||
<div *ngIf="SelectedMembers.length > 0">
|
||||
<p-table [value]="SelectedMembers" [paginator]="true" [rows]="10" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
<c-badge color="secondary">{{value}}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<div class="btn-group" role="group">
|
||||
<button *ngIf="SelectedCloner['direction']=='oneway'" cButton color="warning" size="sm" variant="outline"
|
||||
[cTooltip]="'Set as Master'" (click)="set_master(item.id)" [disabled]="item.id==master">
|
||||
</th>
|
||||
<th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="mac" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>MAC Address</span>
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 100px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid fa-crown me-2 text-warning"
|
||||
*ngIf="SelectedCloner['direction']=='oneway' && item.id==master" pTooltip="Master Device"></i>
|
||||
<i class="fa-solid fa-server me-2 text-primary"
|
||||
*ngIf="!(SelectedCloner['direction']=='oneway' && item.id==master)"></i>
|
||||
<strong>{{item.name}}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td *ngIf="SelectedCloner['pair_type']=='devices'">
|
||||
<c-badge color="secondary">{{item.mac}}</c-badge>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
<button *ngIf="SelectedCloner['direction']=='oneway'" cButton color="warning" size="sm"
|
||||
variant="outline" pTooltip="Set as Master" (click)="set_master(item.id)"
|
||||
[disabled]="item.id==master">
|
||||
<i class="fa-solid fa-crown"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" variant="outline"
|
||||
[cTooltip]="'Remove Member'" (click)="remove_member(item)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
<button cButton color="danger" size="sm" variant="outline" pTooltip="Remove Member"
|
||||
(click)="remove_member(item)">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</div>
|
||||
|
||||
<div class="add-members-section mt-3">
|
||||
<button cButton color="success" (click)="show_new_member_form()">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Members
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td [attr.colspan]="SelectedCloner['pair_type']=='devices' ? 3 : 2" class="text-center p-2">No members
|
||||
added.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
|
||||
<div class="add-members-section mt-3">
|
||||
<button cButton color="success" (click)="show_new_member_form()">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Members
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
|
||||
<button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
|
||||
<button [cModalToggle]="EditClonerModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="SelectedCloner['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
|
||||
<button *ngIf="SelectedCloner['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
|
||||
<button [cModalToggle]="EditClonerModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
|
||||
<c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
|
||||
<c-modal-header class="bg-success text-white">
|
||||
<h5 cModalTitle><i class="fa-solid fa-user-plus me-2"></i>Add Members to Cloner</h5>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-4">
|
||||
<!-- Selection Summary -->
|
||||
<div class="mb-3" style="min-height: 58px;">
|
||||
<c-alert *ngIf="NewMemberRows.length > 0" color="info" class="d-flex align-items-center mb-0">
|
||||
<i class="fa-solid fa-info-circle me-2"></i>
|
||||
<span><strong>{{NewMemberRows.length}}</strong> {{SelectedCloner['pair_type']}} selected for addition</span>
|
||||
</c-alert>
|
||||
</div>
|
||||
<c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
|
||||
<c-modal-header class="bg-success text-white">
|
||||
<h5 cModalTitle><i class="fa-solid fa-user-plus me-2"></i>Add Members to Cloner</h5>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-4">
|
||||
<!-- Selection Summary -->
|
||||
<div class="mb-3" style="min-height: 58px;">
|
||||
<c-alert *ngIf="NewMemberRows.length > 0" color="info" class="d-flex align-items-center mb-0">
|
||||
<i class="fa-solid fa-info-circle me-2"></i>
|
||||
<span><strong>{{NewMemberRows.length}}</strong> {{SelectedCloner['pair_type']}} selected for addition</span>
|
||||
</c-alert>
|
||||
</div>
|
||||
|
||||
<!-- Available Members -->
|
||||
<c-card>
|
||||
<c-card-header class="bg-light">
|
||||
<h6 class="mb-0">
|
||||
<i class="fa-solid me-2" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'" [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
|
||||
Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total)
|
||||
</h6>
|
||||
</c-card-header>
|
||||
<c-card-body class="p-0">
|
||||
<div *ngIf="availbleMembers.length == 0" class="text-center p-4 text-muted">
|
||||
<i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i>
|
||||
<h6>All {{SelectedCloner['pair_type']}} are already added</h6>
|
||||
<p class="mb-0">No available {{SelectedCloner['pair_type']}} to add to this cloner</p>
|
||||
<!-- Available Members -->
|
||||
<c-card>
|
||||
<c-card-header class="bg-light">
|
||||
<h6 class="mb-0">
|
||||
<i class="fa-solid me-2" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'"
|
||||
[class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
|
||||
Available {{SelectedCloner['pair_type'] | titlecase}} ({{availbleMembers.length}} total)
|
||||
</h6>
|
||||
</c-card-header>
|
||||
<c-card-body class="p-0">
|
||||
<div *ngIf="availbleMembers.length == 0" class="text-center p-4 text-muted">
|
||||
<i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i>
|
||||
<h6>All {{SelectedCloner['pair_type']}} are already added</h6>
|
||||
<p class="mb-0">No available {{SelectedCloner['pair_type']}} to add to this cloner</p>
|
||||
</div>
|
||||
<div *ngIf="availbleMembers.length > 0">
|
||||
<div class="mb-3 d-flex justify-content-end p-2">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search available..."
|
||||
(input)="applyFilterGlobalNewMembers($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="availbleMembers.length > 0">
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
|
||||
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true
|
||||
[paging]="paging">
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid me-2 text-primary" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'" [class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
|
||||
<strong>{{value}}</strong>
|
||||
|
||||
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm"
|
||||
[(selection)]="SelectedNewMemberRows" (selectionChange)="onSelectedRowsNewMembers($event)"
|
||||
[globalFilterFields]="['name', 'ip', 'mac']">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
<c-badge color="secondary">{{value}}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedCloner['pair_type']=='devices'" header="MAC Address" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
<small class="text-muted">{{value}}</small>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-modal-body>
|
||||
</th>
|
||||
<th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="ip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>IP Address</span>
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th *ngIf="SelectedCloner['pair_type']=='devices'" pSortableColumn="mac" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>MAC Address</span>
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<c-modal-footer class="bg-light d-flex justify-content-between">
|
||||
<div>
|
||||
<small class="text-muted">Select {{SelectedCloner['pair_type']}} from the list above to add them to the cloner</small>
|
||||
</div>
|
||||
<div>
|
||||
<button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} {{SelectedCloner['pair_type'] | titlecase}}
|
||||
</button>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2">
|
||||
<i class="fa-solid fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid me-2 text-primary" [class.fa-server]="SelectedCloner['pair_type'] == 'devices'"
|
||||
[class.fa-layer-group]="SelectedCloner['pair_type'] != 'devices'"></i>
|
||||
<strong>{{item.name}}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td *ngIf="SelectedCloner['pair_type']=='devices'">
|
||||
<c-badge color="secondary">{{item.ip}}</c-badge>
|
||||
</td>
|
||||
<td *ngIf="SelectedCloner['pair_type']=='devices'">
|
||||
<small class="text-muted">{{item.mac}}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td [attr.colspan]="SelectedCloner['pair_type']=='devices' ? 4 : 2" class="text-center p-4">No available
|
||||
members.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-modal-body>
|
||||
|
||||
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Confirm delete {{ SelectedCloner['name'] }}</h5>
|
||||
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
Are you sure that You want to delete following task ?
|
||||
<br />
|
||||
<br />
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td><b>Taks name : </b></td>
|
||||
<td>{{ SelectedCloner['name'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Description : </b></td>
|
||||
<td>{{ SelectedCloner['description'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Cron exec : </b></td>
|
||||
<td>{{ SelectedCloner['desc_cron'] }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="confirm_delete('',true)" cButton color="danger">
|
||||
Yes,Delete!
|
||||
<c-modal-footer class="bg-light d-flex justify-content-between">
|
||||
<div>
|
||||
<small class="text-muted">Select {{SelectedCloner['pair_type']}} from the list above to add them to the
|
||||
cloner</small>
|
||||
</div>
|
||||
<div>
|
||||
<button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} {{SelectedCloner['pair_type'] | titlecase}}
|
||||
</button>
|
||||
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info">
|
||||
Close
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2">
|
||||
<i class="fa-solid fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
</div>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
|
||||
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Confirm delete {{ SelectedCloner['name'] }}</h5>
|
||||
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
Are you sure that You want to delete following task ?
|
||||
<br />
|
||||
<br />
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td><b>Taks name : </b></td>
|
||||
<td>{{ SelectedCloner['name'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Description : </b></td>
|
||||
<td>{{ SelectedCloner['description'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Cron exec : </b></td>
|
||||
<td>{{ SelectedCloner['desc_cron'] }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="confirm_delete('',true)" cButton color="danger">
|
||||
Yes,Delete!
|
||||
</button>
|
||||
<button [cModalToggle]="DeleteConfirmModal.id" cButton color="info">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
|
|
@ -1,19 +1,8 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChildren ,QueryList } from "@angular/core";
|
||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSelectedRow,
|
||||
GuiSearching,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
|
||||
|
||||
|
|
@ -27,10 +16,13 @@ import { AppToastComponent } from "../toast-simple/toast.component";
|
|||
|
||||
})
|
||||
export class ClonerComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public ispro: boolean = false;
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
@ViewChild('dtNewMembers') tableNewMembers!: Table;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
private router: Router,
|
||||
|
|
@ -62,7 +54,6 @@ export class ClonerComponent implements OnInit {
|
|||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public SelectedCloner: any = {};
|
||||
|
|
@ -74,9 +65,17 @@ export class ClonerComponent implements OnInit {
|
|||
public NewMemberModalVisible: boolean = false;
|
||||
public availbleMembers: any = [];
|
||||
public NewMemberRows: any = [];
|
||||
public SelectedNewMemberRows: any;
|
||||
public SelectedNewMemberRows: any[] = [];
|
||||
public master: number = 0;
|
||||
public active_commands:any=[];
|
||||
public active_commands: any = [];
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
delay: 3000,
|
||||
position: "fixed",
|
||||
fade: true,
|
||||
closeButton: true,
|
||||
};
|
||||
public tabs:any=[
|
||||
{
|
||||
"name": "Network",
|
||||
|
|
@ -186,59 +185,13 @@ export class ClonerComponent implements OnInit {
|
|||
}
|
||||
];
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
options: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Snippet",
|
||||
searchEnabled: true,
|
||||
enableDarkMode: false,
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
delay: 3000,
|
||||
position: "fixed",
|
||||
fade: true,
|
||||
closeButton: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobalNewMembers($event: any, stringVal: string) {
|
||||
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
activate_command(command:string){
|
||||
// add to active_commands if it not added before
|
||||
if(!this.active_commands.includes(command)){
|
||||
|
|
@ -340,9 +293,9 @@ export class ClonerComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRowsNewMembers(rows: any[]): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
|
||||
this.SelectedNewMemberRows = rows;
|
||||
}
|
||||
|
||||
add_new_members() {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ import {
|
|||
} from "@coreui/angular";
|
||||
import { ClonerRoutingModule } from "./cloner-routing.module";
|
||||
import { ClonerComponent } from "./cloner.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
import { NgxSuperSelectModule} from "ngx-super-select";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -31,7 +34,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
ModalModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
|
|
@ -42,6 +47,7 @@ import { NgxSuperSelectModule} from "ngx-super-select";
|
|||
TabsModule,
|
||||
BadgeModule,
|
||||
AlertModule,
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [ClonerComponent],
|
||||
providers: [TitleCasePipe],
|
||||
|
|
|
|||
|
|
@ -142,98 +142,179 @@
|
|||
</form>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<div *ngIf="!chart_data.datasets" style="height: 250px;">
|
||||
<div *ngIf="!chart_data.datasets" style="height: 230px;">
|
||||
<div class="placeholder-glow" style="height: 100%;">
|
||||
<div class="placeholder col-12" style="height: 250px;"></div>
|
||||
<div class="placeholder col-12" style="height: 230px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<c-chart *ngIf="chart_data.datasets" [data]="chart_data" [options]="options" [height]="250" type="line">
|
||||
<c-chart *ngIf="chart_data.datasets" [data]="chart_data" [options]="options" [height]="230" type="line">
|
||||
</c-chart>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
<c-row>
|
||||
<c-col xl="6" lg="12" class="h-100" style="min-height: 160px!important;display: grid">
|
||||
<c-card class="mb-1 p-1 h-100" style="padding-left: 5px!important;">
|
||||
<div class="my-1">
|
||||
<h4 style="display: inline-block;">Version and Serial information</h4>
|
||||
</div>
|
||||
<div *ngIf="!stats">
|
||||
<div class="placeholder-glow">
|
||||
<div class="placeholder col-8"></div>
|
||||
<div class="placeholder col-6"></div>
|
||||
<div class="placeholder col-10"></div>
|
||||
<div class="placeholder col-7"></div>
|
||||
<c-row class="align-items-stretch">
|
||||
<c-col xl="6" lg="12" class="d-flex flex-column mb-3 mb-xl-0" style="min-height: 160px!important;">
|
||||
<c-card class="flex-grow-1 border-0 shadow-sm"
|
||||
style="margin: 0 !important; overflow: hidden; background: linear-gradient(to bottom right, #ffffff, #f8f9fa);">
|
||||
<c-card-body class="d-flex flex-column justify-content-between p-2 px-3">
|
||||
<!-- Skeleton loader -->
|
||||
<div *ngIf="!stats">
|
||||
<div class="placeholder-glow">
|
||||
<div class="placeholder col-8 mb-2"></div>
|
||||
<div class="placeholder col-6 mb-2"></div>
|
||||
<div class="placeholder col-10 mb-2"></div>
|
||||
<div class="placeholder col-7"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!stats['license']" class="my-1">
|
||||
<div style="display: inline-block;margin-right: 5px;">
|
||||
<code style="padding: 0!important;">Serial:</code> <small
|
||||
style="background-color: #ccc;padding: 5px;border-radius: 5px;cursor: pointer;" (click)="copy_this()"
|
||||
[cdkCopyToClipboard]="stats['serial']">{{ stats['serial'] }}</small>
|
||||
<span *ngIf="copy_msg" style="color: #fff!important;" class="badge text-bg-success"><i
|
||||
class="fa-solid fa-check"></i>Copy</span>
|
||||
</div>
|
||||
<c-badge *ngIf="stats['username']" color="danger">Not Registred</c-badge>
|
||||
<c-badge *ngIf="!stats['username']" color="danger">License Validation failed</c-badge>
|
||||
</div>
|
||||
<div *ngIf="stats['license']=='connection_error'" class="my-1">
|
||||
<div style="display: inline-block;margin-right: 5px;">
|
||||
<code style="padding: 0!important;">Serial:</code> <small
|
||||
style="background-color: #ccc;padding: 5px;border-radius: 5px;cursor: pointer;" (click)="copy_this()"
|
||||
[cdkCopyToClipboard]="stats['serial']">{{ stats['serial'] }}</small>
|
||||
<span *ngIf="copy_msg" style="color: #fff!important;" class="badge text-bg-success mx-1"><i
|
||||
class="fa-solid fa-check"></i>Copy</span>
|
||||
</div>
|
||||
<c-badge class="mx-1" color="danger">Unable connect to server/Check server internet connection</c-badge>
|
||||
</div>
|
||||
<div *ngIf="stats['license']!='connection_error' && stats['license']" class="my-1">
|
||||
<div style="display: inline-block;margin-right: 5px;">
|
||||
<code style="padding: 0!important;">Serial:</code> <small
|
||||
style="background-color: #ccc;padding: 5px;border-radius: 5px;cursor: pointer;" (click)="copy_this()"
|
||||
[cdkCopyToClipboard]="stats['serial']">{{ stats['serial'] }}</small>
|
||||
<span *ngIf="copy_msg" style="color: #fff!important;" class="badge text-bg-success mx-1"><i
|
||||
class="fa-solid fa-check"></i>Copy</span>
|
||||
</div>
|
||||
<c-badge color="success">Registred</c-badge>
|
||||
<c-badge class="mx-1" color="info">License Type : {{stats['license']}}</c-badge>
|
||||
<c-badge *ngIf="stats['update_mode']!='auto'" color="info">Manual update</c-badge>
|
||||
<c-badge *ngIf="stats['update_mode']=='auto'" color="info">Auto update</c-badge>
|
||||
</div>
|
||||
<div *ngIf="stats['license']!='connection_error'" class="my-1">
|
||||
<span style="font-size: 0.9rem; display: inline-block;margin-right: 5px"><c-badge
|
||||
[color]="stats['update_available'] ? 'success' : 'secondary'"
|
||||
style="margin: 0!important;padding: 8px;height: 27px;">Your Mikroman version : {{stats['version']}}
|
||||
</c-badge>
|
||||
<i class="fa-solid fa-spinner fa-spin" *ngIf="stats['update_inprogress']"></i>
|
||||
<button cButton color="warning"
|
||||
*ngIf="stats['update_mode']!='auto' && stats['update_available'] && !stats['update_inprogress']" size="sm"
|
||||
(click)="showConfirmModal('update_mikroman')"
|
||||
style="font-size: 0.75em;position: relative;left: -4px;top: 1px;border-top-left-radius: 0;border-bottom-left-radius: 0;height: 27px;"><i
|
||||
class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update availble </button>
|
||||
</span>
|
||||
<span style="font-size: 0.9rem; display: inline-block;"><c-badge
|
||||
[color]="stats['front_update_available'] ? 'success' : 'secondary'" style="padding: 8px;height: 27px;"
|
||||
color="secondary">Your Mikrofront version : {{front_version}}
|
||||
</c-badge>
|
||||
<i class="fa-solid fa-spinner fa-spin" *ngIf="stats['front_update_inprogress']"></i>
|
||||
<button cButton color="warning"
|
||||
*ngIf="stats['update_mode']!='auto' && stats['front_update_available'] && !stats['front_update_inprogress']"
|
||||
size="sm" (click)="showConfirmModal('update_mikrofront')"
|
||||
style="font-size: 0.75em;position: relative;left: -4px;top: 1px;border-top-left-radius: 0;border-bottom-left-radius: 0;height: 27px;"><i
|
||||
class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update availble </button>
|
||||
</span>
|
||||
</div>
|
||||
<p *ngIf="!stats['license'] && !stats['username']" style="color: rgb(0, 119, 255);"><strong>License User name is not set in settings <a style="color: rgb(0, 119, 255);" target="_blank" href="https://mikrowizard.com/docs/register-serial-number/" >read more!</a></strong></p>
|
||||
<p *ngIf="!stats['license'] && stats['username']" style="color: rgb(0, 119, 255);"><strong>Serial number not submited<a style="color: rgb(0, 119, 255);" target="_blank" href="https://mikrowizard.com/docs/register-serial-number/" >read more!</a></strong> </p>
|
||||
|
||||
<!-- <div *ngIf="stats['update_mode']!='auto'" class="my-1">
|
||||
<button cButton color="warning" *ngIf="stats['update_available']" size="sm" style="font-size: 1em;"><i class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update <strong>Mikroman</strong> and reload server</button>
|
||||
<button cButton color="warning" *ngIf="stats['front_update_available']" size="sm" style="font-size: 1em;margin-left: 5px;"><i class="fa-regular fa-hand-pointer fa-beat-fade"></i> Update <strong>MikroFront</strong> and reload Page</button>
|
||||
</div> -->
|
||||
<div *ngIf="stats" class="d-flex flex-column h-100">
|
||||
<!-- Header / Serial -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<h6 class="fw-bold mb-0 d-inline-block me-2" style="color: #2c384af0;">Version & License</h6>
|
||||
<div class="text-muted d-inline-block align-items-center" style="font-size: 0.75rem;">
|
||||
<span class="me-1 font-monospace fw-semibold">{{ stats['serial'] }}</span>
|
||||
<button (click)="copy_this()" [cdkCopyToClipboard]="stats['serial']"
|
||||
class="btn btn-sm btn-light py-0 px-2 border-0 rounded-pill shadow-none lh-1"
|
||||
style="font-size: 0.7rem; background-color: #e2e8f0; color: #475569;">
|
||||
<i class="fa-regular fa-copy"></i>
|
||||
</button>
|
||||
<span *ngIf="copy_msg" class="badge bg-success ms-1 fade-in" style="font-size: 0.65rem;"><i
|
||||
class="fa-solid fa-check"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge rounded-pill fw-semibold shadow-sm lh-1"
|
||||
[ngClass]="stats?.license_info?.status === 'connection_error' ? 'text-bg-secondary' : 'text-bg-success'"
|
||||
style="font-size: 0.7rem; padding: 4px 8px;">
|
||||
<i class="fa-solid me-1"
|
||||
[ngClass]="stats?.license_info?.status === 'connection_error' ? 'fa-globe-slash' : 'fa-globe'"></i>
|
||||
{{ stats?.update_mode === 'auto' ? 'Auto Updates On' : 'Manual Updates' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Info Banner -->
|
||||
<div class="p-2 mb-2 rounded-3 position-relative" [ngClass]="{
|
||||
'bg-success bg-opacity-10 border-start border-4 border-success': stats?.license_info?.status === 'Pro' || stats?.license_info?.status === 'Free',
|
||||
'bg-danger bg-opacity-10 border-start border-4 border-danger': stats?.license_info?.status === 'expired' || stats?.license_info?.status === 'connection_error',
|
||||
'bg-warning bg-opacity-10 border-start border-4 border-warning': stats?.license_info?.action_required === 'set_username'
|
||||
}" style="box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong class="mb-0 text-dark d-flex align-items-center" style="font-size: 0.9rem;">
|
||||
<i class="fa-solid flex-shrink-0 me-2" [ngClass]="{
|
||||
'fa-crown text-warning': stats?.license_info?.status === 'Pro',
|
||||
'fa-leaf text-success': stats?.license_info?.status === 'Free',
|
||||
'fa-circle-xmark text-danger': stats?.license_info?.status === 'expired',
|
||||
'fa-triangle-exclamation text-danger': stats?.license_info?.status === 'connection_error',
|
||||
'fa-user-xmark text-warning': stats?.license_info?.status === 'no_username'
|
||||
}"></i>
|
||||
<span *ngIf="stats?.license_info?.status === 'Pro'">Pro License Active</span>
|
||||
<span *ngIf="stats?.license_info?.status === 'Free'">Free License Active</span>
|
||||
<span *ngIf="stats?.license_info?.status === 'expired'">License Expired</span>
|
||||
<span *ngIf="stats?.license_info?.status === 'connection_error'">Connection Error</span>
|
||||
<span *ngIf="stats?.license_info?.status === 'no_username'">Registration Required</span>
|
||||
</strong>
|
||||
<span *ngIf="stats?.license_info?.status === 'Pro' || stats?.license_info?.status === 'Free'"
|
||||
class="badge fw-bold shadow-sm"
|
||||
[ngClass]="stats?.license_info?.status === 'Pro' ? 'bg-warning text-dark' : 'bg-secondary'">
|
||||
{{ stats?.license_info?.status | uppercase }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||
<p class="mb-0 text-dark opacity-100" style="font-size: 0.75rem; line-height: 1.2; font-weight: 500;">
|
||||
{{ stats?.license_info?.message || (stats?.license_info?.status === 'connection_error' ? 'Cannot connect
|
||||
to server to verify license and updates.' : 'License information unavailable.') }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
*ngIf="stats?.license_info?.action_required !== 'none' && stats?.license_info?.status !== 'connection_error'">
|
||||
<a *ngIf="stats?.license_info?.action_required === 'set_username'"
|
||||
href="https://mikrowizard.com/docs/register-serial-number/" target="_blank"
|
||||
class="btn btn-sm btn-warning text-dark py-0 px-2 rounded-pill fw-semibold shadow-sm"
|
||||
style="font-size: 0.7rem;"><i class="fa-solid fa-arrow-up-right-from-square me-1"></i> Setup</a>
|
||||
<a *ngIf="stats?.license_info?.action_required === 'renew_license'"
|
||||
href="https://mikrowizard.com/pricing" target="_blank"
|
||||
class="btn btn-sm btn-danger py-0 px-2 rounded-pill fw-semibold shadow-sm"
|
||||
style="font-size: 0.7rem;"><i class="fa-solid fa-cart-shopping me-1"></i> Renew</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Modules -->
|
||||
<div class="mt-auto row g-2" *ngIf="stats?.license_info?.status !== 'connection_error'">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div
|
||||
class="p-1 px-2 border rounded-3 bg-white shadow-sm d-flex justify-content-between align-items-center h-100"
|
||||
[ngClass]="{'border-success border-2 bg-success bg-opacity-10': stats['update_available']}">
|
||||
<div>
|
||||
<div class="fw-bold text-dark mb-0 d-flex align-items-center lh-1" style="font-size: 0.75rem;">
|
||||
<i class="fa-solid fa-server me-1 text-secondary"></i>Mikroman Backend v{{stats['version']}}
|
||||
</div>
|
||||
<div class="text-muted lh-1 mt-1" style="font-size: 0.65rem;" *ngIf="!stats['update_available']">Up to
|
||||
date</div>
|
||||
<div class="text-success fw-bold lh-1 mt-1" style="font-size: 0.65rem;"
|
||||
*ngIf="stats['update_available']"><i
|
||||
class="fa-solid fa-arrow-up me-1"></i>v{{stats['latest_version']}} Available</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span *ngIf="stats['update_mode'] == 'auto' && stats['update_available']"
|
||||
class="text-muted small fw-semibold" style="font-size: 0.65rem;"><i
|
||||
class="fa-solid fa-clock me-1"></i>Auto-updating</span>
|
||||
<button class="btn btn-sm btn-success rounded-pill fw-semibold py-0 px-2 shadow lh-1"
|
||||
*ngIf="stats['update_mode']!='auto' && stats['update_available'] && !stats['update_inprogress']"
|
||||
(click)="showConfirmModal('update_mikroman')"
|
||||
style="font-size: 0.65rem; background: linear-gradient(45deg, #198754, #20c997); border: none; height: 20px;">
|
||||
<i class="fa-solid fa-download me-1"></i> Update
|
||||
</button>
|
||||
<i class="fa-solid fa-circle-notch fa-spin text-success" style="font-size: 0.8rem;"
|
||||
*ngIf="stats['update_inprogress']"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div
|
||||
class="p-1 px-2 border rounded-3 bg-white shadow-sm d-flex justify-content-between align-items-center h-100"
|
||||
[ngClass]="{'border-success border-2 bg-success bg-opacity-10': stats['front_update_available']}">
|
||||
<div>
|
||||
<div class="fw-bold text-dark mb-0 d-flex align-items-center lh-1" style="font-size: 0.75rem;">
|
||||
<i class="fa-brands fa-angular me-1 text-secondary"></i>MikroFront UI v{{front_version}}
|
||||
</div>
|
||||
<div class="text-muted lh-1 mt-1" style="font-size: 0.65rem;"
|
||||
*ngIf="!stats['front_update_available']">Up to date</div>
|
||||
<div class="text-success fw-bold lh-1 mt-1" style="font-size: 0.65rem;"
|
||||
*ngIf="stats['front_update_available']"><i
|
||||
class="fa-solid fa-arrow-up me-1"></i>v{{stats['front_latest_version']}} Available</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span *ngIf="stats['update_mode'] == 'auto' && stats['front_update_available']"
|
||||
class="text-muted small fw-semibold" style="font-size: 0.65rem;"><i
|
||||
class="fa-solid fa-clock me-1"></i>Auto-updating</span>
|
||||
<button class="btn btn-sm btn-success rounded-pill fw-semibold py-0 px-2 shadow lh-1"
|
||||
*ngIf="stats['update_mode']!='auto' && stats['front_update_available'] && !stats['front_update_inprogress']"
|
||||
(click)="showConfirmModal('update_mikrofront')"
|
||||
style="font-size: 0.65rem; background: linear-gradient(45deg, #198754, #20c997); border: none; height: 20px;">
|
||||
<i class="fa-solid fa-download me-1"></i> Update
|
||||
</button>
|
||||
<i class="fa-solid fa-circle-notch fa-spin text-success" style="font-size: 0.8rem;"
|
||||
*ngIf="stats['front_update_inprogress']"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="stats?.license_info?.status === 'connection_error'" class="mt-auto py-1 text-center text-muted">
|
||||
<div class="small fw-semibold opacity-50" style="font-size: 0.75rem;"><i
|
||||
class="fa-solid fa-network-wired me-1"></i>Local functionality only. Remote services unreachable.</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
<c-col xl="6" lg="12" class="h-100" style="min-height: 160px!important;display: grid;">
|
||||
<c-card class="h-100" style="padding: 0!important;margin: 0!important;">
|
||||
<c-col xl="6" lg="12" class="d-flex flex-column" style="min-height: 160px!important;">
|
||||
<c-card class="flex-grow-1" style="padding: 0!important;margin: 0!important;">
|
||||
<div *ngIf="!stats" style="padding: 20px;">
|
||||
<div class="placeholder-glow">
|
||||
<div class="placeholder col-4" style="height: 150px; float: left; margin-right: 20px;"></div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Address</th>
|
||||
<th scope="col">Att</th>
|
||||
<th scope="col">At</th>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Via</th>
|
||||
|
|
@ -27,8 +27,9 @@
|
|||
<td>{{item['group']}}</td>
|
||||
<td>{{item['name']}}</td>
|
||||
<td>{{item['via']}}</td>
|
||||
<td><button cButton (click)="killsession(item)" style="padding: 0;" size="sm" color="danger" variant="ghost"><i class="fa-solid fa-skull-crossbones"></i>
|
||||
kill</button></td>
|
||||
<td><button cButton (click)="killsession(item)" style="padding: 0;" size="sm" color="danger"
|
||||
variant="ghost"><i class="fa-solid fa-skull-crossbones"></i>
|
||||
kill</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -104,69 +104,97 @@
|
|||
<c-card class="mb-1">
|
||||
<c-card-body>
|
||||
<c-row style="flex-direction: row">
|
||||
<gui-grid [source]="interfaces" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[autoResizeWidth]="true">
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{ value }} - {{ item["default-name"] }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<div class="mb-3 d-flex justify-content-end w-100">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search interfaces..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gui-grid-column header="MAC" field="mac-address">
|
||||
<ng-template let-value="item['mac-address']" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="rx" field="rx-byte">
|
||||
<ng-template let-value="item['rx-byte']" let-item="item" let-index="index">
|
||||
<div>{{ convert_bw_human(value, "rx") }}</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="tx" field="tx-byte">
|
||||
<ng-template let-value="item['tx-byte']" let-item="item" let-index="index">
|
||||
{{ convert_bw_human(value, "tx") }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="l2mtu" field="l2mtu">
|
||||
<ng-template let-value="item.l2mtu" let-item="item" let-index="index">
|
||||
curr:{{ value }}<br />
|
||||
max : {{ item["max-l2mtu"] }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="rx/s" field="rx-bits-per-second" [enabled]="false">
|
||||
<ng-template let-value="item['rx-bits-per-second']" let-item="item" let-index="index">
|
||||
{{ convert_bw_human(value, "rx") }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="tx/s" field="tx-bits-per-second" [enabled]="false">
|
||||
<ng-template let-value="item['tx-bits-per-second']" let-item="item" let-index="index">
|
||||
{{ convert_bw_human(value, "tx") }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Created" field="created" [enabled]="false">
|
||||
<ng-template let-value="item.created" let-item="item.id" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Last Up" field="last-link-up-time">
|
||||
<ng-template let-value="item['last-link-up-time']" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Created" field="created" [enabled]="false">
|
||||
<ng-template let-value="item.created" let-item="item.id" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" field="action" width="60" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="info" size="sm" (click)="show_interface_rate(item['default-name'])"
|
||||
class="mx-1">
|
||||
<p-table #dtInterfaces [value]="interfaces" [paginator]="true" [rows]="10"
|
||||
[showGridlines]="true" [stripedRows]="true"
|
||||
[resizableColumns]="true" columnResizeMode="expand"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name', 'default-name', 'mac-address']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="mac-address" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>MAC</span>
|
||||
<span>
|
||||
<p-sortIcon field="mac-address"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="mac-address" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="rx-byte" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>RX</span>
|
||||
<span>
|
||||
<p-sortIcon field="rx-byte"></p-sortIcon>
|
||||
<p-columnFilter type="numeric" field="rx-byte" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="tx-byte" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>TX</span>
|
||||
<span>
|
||||
<p-sortIcon field="tx-byte"></p-sortIcon>
|
||||
<p-columnFilter type="numeric" field="tx-byte" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="l2mtu" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>L2MTU</span>
|
||||
<span>
|
||||
<p-sortIcon field="l2mtu"></p-sortIcon>
|
||||
<p-columnFilter type="numeric" field="l2mtu" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="last-link-up-time" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Last Up</span>
|
||||
<span>
|
||||
<p-sortIcon field="last-link-up-time"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="last-link-up-time" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 60px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-iface>
|
||||
<tr>
|
||||
<td>{{iface.name}} <small class="text-muted">({{iface['default-name']}})</small></td>
|
||||
<td><small class="font-monospace">{{iface['mac-address']}}</small></td>
|
||||
<td>{{convert_bw_human(iface['rx-byte'], 'rx')}}</td>
|
||||
<td>{{convert_bw_human(iface['tx-byte'], 'tx')}}</td>
|
||||
<td>
|
||||
<small>curr: {{iface.l2mtu}}</small><br/>
|
||||
<small>max: {{iface['max-l2mtu']}}</small>
|
||||
</td>
|
||||
<td><small>{{iface['last-link-up-time']}}</small></td>
|
||||
<td class="text-center">
|
||||
<button cButton color="info" size="sm" (click)="show_interface_rate(iface['default-name'])" pTooltip="Interface Chart">
|
||||
<i class="fa-solid fa-chart-line"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-row>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
|
@ -182,16 +210,16 @@
|
|||
</c-card-header>
|
||||
<c-card-body>
|
||||
<h6>{{ raddata.key }}</h6>
|
||||
<app-widgets-dropdown [devicedata]="raddata.value"></app-widgets-dropdown>
|
||||
<app-widgets-dropdown [devicedata]="$any(raddata.value)"></app-widgets-dropdown>
|
||||
<c-row>
|
||||
<c-col md="3">
|
||||
<table style="word-break: break-word" small stripedColumns cTable>
|
||||
<tbody>
|
||||
<ng-container *ngFor="
|
||||
let d of raddata.value['data'] | keyvalue;
|
||||
let d of $any(raddata.value)['data'] | keyvalue;
|
||||
let i = index
|
||||
">
|
||||
<tr *ngIf="i < objectlen(raddata.value['data']) / 4">
|
||||
<tr *ngIf="i < objectlen($any(raddata.value)['data']) / 4">
|
||||
<th style="width: 20%; text-wrap: nowrap">
|
||||
{{ d.key }}
|
||||
</th>
|
||||
|
|
@ -205,12 +233,12 @@
|
|||
<table style="word-break: break-word" small stripedColumns cTable>
|
||||
<tbody>
|
||||
<ng-container *ngFor="
|
||||
let d of raddata.value['data'] | keyvalue;
|
||||
let d of $any(raddata.value)['data'] | keyvalue;
|
||||
let i = index
|
||||
">
|
||||
<tr *ngIf="
|
||||
i >= objectlen(raddata.value['data']) / 4 &&
|
||||
i < (objectlen(raddata.value['data']) / 4) * 2
|
||||
i >= objectlen($any(raddata.value)['data']) / 4 &&
|
||||
i < (objectlen($any(raddata.value)['data']) / 4) * 2
|
||||
">
|
||||
<th style="width: 20%; text-wrap: nowrap">
|
||||
{{ d.key }}
|
||||
|
|
@ -225,12 +253,12 @@
|
|||
<table style="word-break: break-word" small stripedColumns cTable>
|
||||
<tbody>
|
||||
<ng-container *ngFor="
|
||||
let d of raddata.value['data'] | keyvalue;
|
||||
let d of $any(raddata.value)['data'] | keyvalue;
|
||||
let i = index
|
||||
">
|
||||
<tr *ngIf="
|
||||
i >= (objectlen(raddata.value['data']) / 4) * 2 &&
|
||||
i < (objectlen(raddata.value['data']) / 4) * 3
|
||||
i >= (objectlen($any(raddata.value)['data']) / 4) * 2 &&
|
||||
i < (objectlen($any(raddata.value)['data']) / 4) * 3
|
||||
">
|
||||
<th style="width: 20%; text-wrap: nowrap">
|
||||
{{ d.key }}
|
||||
|
|
@ -245,11 +273,11 @@
|
|||
<table small stripedColumns cTable>
|
||||
<tbody>
|
||||
<ng-container *ngFor="
|
||||
let d of raddata.value['data'] | keyvalue;
|
||||
let d of $any(raddata.value)['data'] | keyvalue;
|
||||
let i = index
|
||||
">
|
||||
<tr *ngIf="
|
||||
i >= (objectlen(raddata.value['data']) / 4) * 3
|
||||
i >= (objectlen($any(raddata.value)['data']) / 4) * 3
|
||||
">
|
||||
<th>{{ d.key }}</th>
|
||||
<td scope="row">{{ d.value }}</td>
|
||||
|
|
@ -259,7 +287,7 @@
|
|||
</table>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-row *ngIf="raddata.value['strength-at-rates']">
|
||||
<c-row *ngIf="$any(raddata.value)['strength-at-rates']">
|
||||
<c-col>
|
||||
<table style="word-break: break-word" small borderless cTable>
|
||||
<tbody>
|
||||
|
|
@ -276,7 +304,7 @@
|
|||
<td scope="row">
|
||||
<c-badge color="info" style="font-size: 0.85em" class="mx-1" *ngFor="
|
||||
let st of strangth_at_rate_extract(
|
||||
raddata.value['strength-at-rates']
|
||||
$any(raddata.value)['strength-at-rates']
|
||||
)
|
||||
">{{ st }}</c-badge>
|
||||
</td>
|
||||
|
|
@ -292,8 +320,7 @@
|
|||
</c-tab-pane>
|
||||
<c-tab-pane>
|
||||
<ng-container *ngIf="dhcp_server_available">
|
||||
<app-dhcp-info [tz]="tz" [dhcp_server_data]="dhcp_server_data" [small_screen]="small_screen" [columnMenu]="columnMenu"
|
||||
[sorting]="sorting" [searching]="searching" [infoPanel]="infoPanel" [paging]="paging"></app-dhcp-info>
|
||||
<app-dhcp-info [tz]="tz" [dhcp_server_data]="dhcp_server_data" [small_screen]="small_screen"></app-dhcp-info>
|
||||
</ng-container>
|
||||
</c-tab-pane>
|
||||
<c-tab-pane style="width: 100%;padding-top: 10px;background-color: #fff;">
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@
|
|||
cursor: pointer;
|
||||
|
||||
}
|
||||
.interfaces gui-grid gui-structure{
|
||||
min-height: unset!important;
|
||||
}
|
||||
.nav-underline .nav-link:hover,.nav-underline .nav-link:focus {
|
||||
border-color: var(--cui-nav-underline-link-active-border-color, #321fdb)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,8 @@
|
|||
import { Component, OnDestroy, OnInit ,ViewEncapsulation} from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation, ViewChild } from "@angular/core";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSearching,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { __setFunctionName } from "tslib";
|
||||
interface IUser {
|
||||
name: string;
|
||||
|
|
@ -38,10 +28,10 @@ type radiodata = {
|
|||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class DeviceComponent implements OnInit, OnDestroy {
|
||||
public uid: number;
|
||||
public uid!: number;
|
||||
public sessionloaded: boolean = false;
|
||||
public uname: string;
|
||||
public tz: string;
|
||||
public uname!: string;
|
||||
public tz!: string;
|
||||
public ispro: boolean = false;
|
||||
public small_screen=false;
|
||||
public show_dev_logs: boolean = false;
|
||||
|
|
@ -81,8 +71,11 @@ export class DeviceComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
public devdata: any;
|
||||
public devsensors: any;
|
||||
public radio_devsensors: radiodata;
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public radiodata!: radiodata;
|
||||
public radio_devsensors!: any;
|
||||
|
||||
@ViewChild('dtInterfaces') dtInterfaces!: Table;
|
||||
|
||||
public loading: boolean = true;
|
||||
public radio_loading: boolean = true;
|
||||
public InterfaceChartModalVisible: boolean = false;
|
||||
|
|
@ -98,45 +91,11 @@ export class DeviceComponent implements OnInit, OnDestroy {
|
|||
public dhcp_server_available: boolean = false;
|
||||
public dhcp_server_data: any = {};
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
public interfaces: Array<any> = [];
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
pageSizes: [20],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
objectlen(object:any){
|
||||
return Object.keys(object).length;
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.dtInterfaces.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
strangth_at_rate_extract(data:string){
|
||||
return data.split(',');
|
||||
}
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search In table",
|
||||
};
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
reload_dhcp_server(){
|
||||
this.get_DHCP_data();
|
||||
}
|
||||
|
|
@ -553,6 +512,12 @@ export class DeviceComponent implements OnInit, OnDestroy {
|
|||
show_history(itme:any) {
|
||||
return
|
||||
}
|
||||
objectlen(object:any){
|
||||
return object ? Object.keys(object).length : 0;
|
||||
}
|
||||
strangth_at_rate_extract(data:string){
|
||||
return data ? data.split(',') : [];
|
||||
}
|
||||
ngOnDestroy() {
|
||||
clearInterval(this.data_interval);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import { ChartjsModule } from "@coreui/angular-chartjs";
|
|||
|
||||
import { DeviceRoutingModule } from "./device-routing.module";
|
||||
import { DeviceComponent } from "./device.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
import { WidgetsModule } from "../widgets/widgets.module";
|
||||
import { DeviceInfoModule } from "./device-info/device-info.module";
|
||||
|
|
@ -53,7 +55,9 @@ import { AccModule } from "../acc_log/acc.module";
|
|||
AuthModule,
|
||||
AccModule,
|
||||
SpinnerModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
NavbarModule,
|
||||
ModalModule,
|
||||
TableModule,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<ng-container *ngFor="let item of dhcp_server_data ">
|
||||
<c-row style="padding: 10px 0;" >
|
||||
<c-row style="padding: 10px 0;">
|
||||
<c-col xl="3" sm="12">
|
||||
<c-row *ngIf="small_screen">
|
||||
<c-col sm="6" style="padding-right: 0;">
|
||||
|
|
@ -77,51 +77,101 @@
|
|||
<c-col xl="9" sm="12">
|
||||
<c-card class="mb-1 h-100">
|
||||
<c-card-body>
|
||||
<gui-grid [source]="item['lease_table']" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[searching]="searching" [infoPanel]="infoPanel" [rowHeight]="28" [autoResizeWidth]=true [paging]="paging">
|
||||
<gui-grid-column header="address" field="address">
|
||||
<ng-template let-value="item.address" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Type" field="dynamic">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
<span *ngIf="item['dynamic']">Dynamic</span>
|
||||
<span *ngIf="item['static']">Static</span>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="expire" field="expires-after">
|
||||
<ng-template let-value="item['expires-after']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="host-name" field="host-name">
|
||||
<ng-template let-value="item['host-name']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="last-seen" field="last-seen">
|
||||
<ng-template let-value="item['last-seen']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="status" field="status">
|
||||
<ng-template let-value="item['status']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="MAC" field="mac-address">
|
||||
<ng-template let-value="item['mac-address']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="60" field="">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
<button cButton color="info" size="sm" (click)="show_history(item)" class="mx-1"><i
|
||||
class="fa-solid fa-clock-rotate-left"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search leases..."
|
||||
(input)="applyFilterGlobalLeases($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtLeases [value]="item['lease_table']" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['address', 'host-name', 'mac-address', 'status']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="address" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Address</span>
|
||||
<span>
|
||||
<p-sortIcon field="address"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="address" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="dynamic" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Type</span>
|
||||
<span>
|
||||
<p-sortIcon field="dynamic"></p-sortIcon>
|
||||
<p-columnFilter type="boolean" field="dynamic" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="expires-after" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Expire</span>
|
||||
<span>
|
||||
<p-sortIcon field="expires-after"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="expires-after" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="host-name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Host Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="host-name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="host-name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="status" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Status</span>
|
||||
<span>
|
||||
<p-sortIcon field="status"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="status" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="mac-address" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>MAC</span>
|
||||
<span>
|
||||
<p-sortIcon field="mac-address"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="mac-address" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 60px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-lease>
|
||||
<tr>
|
||||
<td>{{lease.address}}</td>
|
||||
<td>
|
||||
<c-badge [color]="lease.dynamic ? 'info' : 'secondary'">
|
||||
{{lease.dynamic ? 'Dynamic' : 'Static'}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td>{{lease['expires-after']}}</td>
|
||||
<td>{{lease['host-name']}}</td>
|
||||
<td>
|
||||
<c-badge [color]="lease.status === 'bound' ? 'success' : 'warning'">
|
||||
{{lease.status}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td><small class="font-monospace">{{lease['mac-address']}}</small></td>
|
||||
<td class="text-center">
|
||||
<button cButton color="info" size="sm" (click)="show_history(lease)" pTooltip="View History">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
@ -129,36 +179,60 @@
|
|||
</ng-container>
|
||||
|
||||
|
||||
<c-modal #DhcpHistoryModal backdrop="static" size="xl" [visible]="dhcp_history_modal"
|
||||
id="DhcpHistoryModal" *ngIf="dhcp_history_modal">
|
||||
<c-modal #DhcpHistoryModal backdrop="static" size="xl" [visible]="dhcp_history_modal" id="DhcpHistoryModal"
|
||||
*ngIf="dhcp_history_modal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>History for {{current_dhcp['mac-address']}}</h5>
|
||||
<button [cModalToggle]="DhcpHistoryModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body style="overflow-x: auto;">
|
||||
<gui-grid style="min-width: 900px;" [source]="dhcp_history" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[searching]="searching" [infoPanel]="infoPanel" [rowHeight]="35" [autoResizeWidth]=true [paging]="paging">
|
||||
<gui-grid-column header="comment" field="comment" >
|
||||
<ng-template let-value="item.comment" let-item="item" let-index="index">
|
||||
<div style="text-wrap: auto;font-weight: bold;line-height: normal;">{{value}}</div>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search history..."
|
||||
(input)="applyFilterGlobalHistory($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtHistory [value]="dhcp_history" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['comment', 'detail', 'eventtime', 'ip', 'name']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="comment" pResizableColumn>
|
||||
Comment
|
||||
<p-sortIcon field="comment"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="comment" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="detail" style="width: 150px" pResizableColumn>
|
||||
Type
|
||||
<p-sortIcon field="detail"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="detail" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="eventtime" style="width: 200px" pResizableColumn>
|
||||
Event Time
|
||||
<p-sortIcon field="eventtime"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="eventtime" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="ip" style="width: 250px" pResizableColumn>
|
||||
Router
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="type" field="detail" width="150">
|
||||
<ng-template let-value="item['detail']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
<ng-template pTemplate="body" let-hist>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="text-wrap: auto; font-weight: bold;">{{hist.comment}}</div>
|
||||
</td>
|
||||
<td>{{hist.detail}}</td>
|
||||
<td>{{hist.eventtime}}</td>
|
||||
<td>{{hist.name}} ({{hist.ip}})</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="eventtime" field="eventtime" width="200">
|
||||
<ng-template let-value="item['eventtime']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Router" field="ip" width="250">
|
||||
<ng-template let-value="item['ip']" let-item="item" let-index="index">
|
||||
{{item.name}} ({{value}})
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</p-table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button [cModalToggle]="DhcpHistoryModal.id" cButton color="secondary">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component,Input } from '@angular/core';
|
||||
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, ViewChild } from '@angular/core';
|
||||
import { dataProvider } from "../../../providers/mikrowizard/data";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dhcp-info',
|
||||
|
|
@ -11,13 +12,11 @@ import { formatInTimeZone } from "date-fns-tz";
|
|||
export class DhcpInfoComponent implements AfterContentInit {
|
||||
@Input() dhcp_server_data: any;
|
||||
@Input() small_screen: boolean=false;
|
||||
@Input() columnMenu: any;
|
||||
@Input() sorting: any;
|
||||
@Input() searching: any;
|
||||
@Input() infoPanel: any;
|
||||
@Input() paging: any;
|
||||
@Input() rowSelectionMode: any;
|
||||
@Input() tz: any;
|
||||
|
||||
@ViewChild('dtLeases') dtLeases!: Table;
|
||||
@ViewChild('dtHistory') dtHistory!: Table;
|
||||
|
||||
dhcp_history:any;
|
||||
dhcp_history_modal: boolean = false;
|
||||
current_dhcp:any;
|
||||
|
|
@ -26,7 +25,14 @@ export class DhcpInfoComponent implements AfterContentInit {
|
|||
private data_provider: dataProvider,
|
||||
) {}
|
||||
|
||||
show_history(item:any) {
|
||||
applyFilterGlobalLeases($event: any, stringVal: string) {
|
||||
this.dtLeases.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
applyFilterGlobalHistory($event: any, stringVal: string) {
|
||||
this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
show_history(item: any) {
|
||||
var _self = this;
|
||||
this.data_provider.getDhcpHistory(item).then((res) => {
|
||||
_self.current_dhcp = item;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import { WidgetsModule } from "../../widgets/widgets.module";
|
|||
|
||||
// import { WidgetsRoutingModule } from './widgets-routing.module';
|
||||
import { DhcpInfoComponent } from './dhcp-info.component';
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
|
@ -42,7 +44,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
WidgetsModule,
|
||||
NavbarModule,
|
||||
ModalModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
TableModule,
|
||||
UtilitiesModule,
|
||||
BadgeModule,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
{{ping['successful_pings']}}</c-badge></h6>
|
||||
<h6 style="display: inline-block;margin-left: 0.5rem;"> | Failed pings : <c-badge color="danger"
|
||||
style="font-size: 90%;"> {{ping['failed_pings']}}</c-badge></h6>
|
||||
<h6 style="display: inline-block;margin-left: 0.5rem;">| Avrage : <c-badge color="dark" style="font-size: 90%;">
|
||||
<h6 style="display: inline-block;margin-left: 0.5rem;">| Average : <c-badge color="dark"
|
||||
style="font-size: 90%;">
|
||||
{{ping['average_ping_time']}} ms</c-badge></h6>
|
||||
<table cTable small>
|
||||
<thead>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
|
||||
<h5>Device LOGS
|
||||
<h5>Device Logs
|
||||
<a style="cursor: pointer;" (click)="reinitgrid('none','none')"><i
|
||||
*ngIf="devid!=0 && component_devid && !reloading" class="fa-solid fa-arrows-rotate"
|
||||
style="color: #74C0FC;"></i>
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
</h5>
|
||||
<c-badge color="warning" *ngIf="devid!=0 && !component_devid">Filtered Result For Device ID
|
||||
{{devid}}</c-badge>
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time.
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
|
||||
*ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
|
||||
default. Use filters to modify the date and time.
|
||||
</c-alert>
|
||||
</c-col>
|
||||
<c-col xs [lg]="1">
|
||||
|
|
@ -84,74 +86,207 @@
|
|||
</div>
|
||||
|
||||
</c-row>
|
||||
<gui-grid wid [rowDetail]="rowDetail" [horizontalGrid]="true" [rowHeight]="52" [source]="source"
|
||||
[columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[autoResizeWidth]="true">
|
||||
<gui-grid-column header="#No" type="NUMBER" field="index" width="1" align="CENTER">
|
||||
<ng-template let-value="item.index" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gui-grid-column header="level" width='90' wid field="level">
|
||||
<ng-template let-value="item.level" let-item="item" let-index="index">
|
||||
<c-badge style="cursor: pointer; font-weight: normal" color="danger" *ngIf="value == 'Critical'">{{ value
|
||||
}}</c-badge>
|
||||
<c-badge style="cursor: pointer; font-weight: normal" color="warning" *ngIf="value == 'Error'">{{ value
|
||||
}}</c-badge>
|
||||
<c-badge style="cursor: pointer; font-weight: normal" color="warning" *ngIf="value == 'Warning'">{{ value
|
||||
}}</c-badge>
|
||||
<c-badge style="cursor: pointer; font-weight: normal; min-width: 60px;" color="info"
|
||||
*ngIf="value == 'info'">{{ value }}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Event" width='200' field="detail">
|
||||
<ng-template let-value="item.detail" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Detail" field="comment">
|
||||
<ng-template let-value="item.comment" let-item="item" let-index="index">
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['detail', 'comment', 'src', 'name', 'devip', 'level']" selectionMode="single"
|
||||
(onRowSelect)="showLogDetails($event.data)">
|
||||
|
||||
<div class="gui-dev-info">
|
||||
{{ value }}
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>#No</span>
|
||||
<p-sortIcon field="index"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="level" style="width: 100px" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Level</span>
|
||||
<span>
|
||||
<p-sortIcon field="level"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="level" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="detail" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Event</span>
|
||||
<span>
|
||||
<p-sortIcon field="detail"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="detail" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="comment" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Detail</span>
|
||||
<span>
|
||||
<p-sortIcon field="comment"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="comment" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="src" style="width: 100px" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Source</span>
|
||||
<span>
|
||||
<p-sortIcon field="src"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="src" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="status" style="width: 100px" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Status</span>
|
||||
<span>
|
||||
<p-sortIcon field="status"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="status" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="eventtime" style="width: 200px" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Time</span>
|
||||
<span>
|
||||
<p-sortIcon field="eventtime"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="eventtime" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Device</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr [pSelectableRow]="item" style="cursor: pointer;">
|
||||
<td class="text-center">{{item.index}}</td>
|
||||
<td>
|
||||
<c-badge
|
||||
[color]="item.level == 'Critical' ? 'danger' : (item.level == 'Error' || item.level == 'Warning' ? 'warning' : 'info')"
|
||||
style="font-weight: normal; min-width: 65px;">
|
||||
{{item.level}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td>{{item.detail}}</td>
|
||||
<td>
|
||||
<div class="text-truncate" style="max-width: 250px;" [pTooltip]="item.comment" tooltipPosition="top">
|
||||
{{item.comment}}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{item.src}}</td>
|
||||
<td class="text-center">
|
||||
<c-badge [color]="item.status == true ? 'success' : 'danger'" style="font-weight: normal">
|
||||
{{item.status == true ? 'Fixed' : 'Not Fixed'}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td class="small">
|
||||
<div *ngIf="item.fixtime" class="d-flex flex-column gap-1">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<c-badge color="danger" style="font-size: 0.6rem; padding: 2px 4px;">EVT</c-badge>
|
||||
<span>{{item.eventtime}}</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<c-badge color="success" style="font-size: 0.6rem; padding: 2px 4px;">FIX</c-badge>
|
||||
<span>{{item.fixtime}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!item.fixtime">{{item.eventtime}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{item.name}}</div>
|
||||
<div class="small text-muted">{{item.devip}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center p-4">No device logs found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<!-- Details Drawer -->
|
||||
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '500px'}" header="Device Log Details">
|
||||
<div *ngIf="selectedLog" class="p-0">
|
||||
<div [style.background-color]="getSeverityColor(selectedLog.level)" class="p-4 text-white shadow-sm mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="m-0"><i class="pi pi-exclamation-triangle me-2"></i>{{selectedLog.level}} Alert</h3>
|
||||
<div class="opacity-75">{{selectedLog.eventtype || 'System Event'}}</div>
|
||||
</div>
|
||||
<i class="pi pi-bell" style="font-size: 2.5rem; opacity: 0.3;"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Source" width='90' field="src">
|
||||
<ng-template let-value="item.src" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="status" width='100' field="status" align="CENTER">
|
||||
<ng-template let-value="item.status" let-item="item" let-index="index">
|
||||
<c-badge style=" cursor: pointer; font-weight: normal" color="success"
|
||||
*ngIf="value == true">Fixed</c-badge>
|
||||
<c-badge style="cursor: pointer; font-weight: normal" color="danger" *ngIf="value != true">Not
|
||||
Fixed</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="eventtime" width='220' field="eventtime">
|
||||
<ng-template let-value="item.eventtime" let-item="item" let-index="index">
|
||||
<div *ngIf="item.fixtime" class="fixed_time"><span><c-badge *ngIf="item.status==true" color="danger"
|
||||
style="font-size: 0.65em!important;padding: 3px 5px;" size="sm" shape="rounded-pill">Event</c-badge>
|
||||
{{value}}</span><span>
|
||||
<c-badge *ngIf="item.status==true" color="success"
|
||||
style="font-size: 0.65em!important;padding: 3px 5px;" size="sm" shape="rounded-pill">Fixed</c-badge>
|
||||
{{item.fixtime}}</span>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-desktop me-2 text-primary"></i>Device Info</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">Device Name:</div>
|
||||
<div class="col-7 fw-bold">{{selectedLog.name}}</div>
|
||||
</div>
|
||||
<div *ngIf="!item.fixtime"> {{ value }}</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device" width='200' field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<div class="gui-dev-info">
|
||||
<span class="gui-dev-info-name">{{ value }}</span>
|
||||
<span class="gui-dev-info-ip">{{ item.devip }}</span>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">IP Address:</div>
|
||||
<div class="col-7 font-monospace">{{selectedLog.devip}}</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">MAC Address:</div>
|
||||
<div class="col-7 small font-monospace">{{selectedLog.mac}}</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-info-circle me-2 text-primary"></i>Event Details
|
||||
</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">Event:</div>
|
||||
<div class="col-7">{{selectedLog.detail}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">Source:</div>
|
||||
<div class="col-7">{{selectedLog.src}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">Event Time:</div>
|
||||
<div class="col-7 small">{{selectedLog.eventtime}}</div>
|
||||
</div>
|
||||
<div *ngIf="selectedLog.fixtime" class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">Fixed Time:</div>
|
||||
<div class="col-7 small text-success fw-bold">{{selectedLog.fixtime}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-5 fw-bold text-muted small text-uppercase">Status:</div>
|
||||
<div class="col-7">
|
||||
<c-badge [color]="selectedLog.status == true ? 'success' : 'danger'" style="padding: 5px 10px;">
|
||||
{{selectedLog.status == true ? 'Fixed' : 'Not Fixed'}}
|
||||
</c-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-comment me-2 text-primary"></i>Detail Message
|
||||
</h5>
|
||||
<div class="bg-light p-3 rounded border italic text-muted text-break"
|
||||
style="font-size: 0.9rem; line-height: 1.4;">
|
||||
{{selectedLog.comment || 'No additional details available for this event.'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-drawer>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,9 @@
|
|||
import { Component, OnInit, ViewEncapsulation,Input } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild, ViewEncapsulation, Input } from "@angular/core";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiRowDetail,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
|
@ -27,10 +17,14 @@ import { Subject } from "rxjs";
|
|||
})
|
||||
export class DevLogsComponent implements OnInit {
|
||||
@Input() component_devid: any=false;
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid!: number;
|
||||
public uname!: string;
|
||||
public tz: string = "UTC"
|
||||
public filterText: string;
|
||||
public filterText!: string;
|
||||
public detailsVisible: boolean = false;
|
||||
public selectedLog: any = null;
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
public filters: any = {
|
||||
start_time: false,
|
||||
end_time: false,
|
||||
|
|
@ -73,108 +67,28 @@ export class DevLogsComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
public selected_rows: any[] = [];
|
||||
public Selectedrows: any[] = [];
|
||||
public devid: number = 0;
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
public bankMultiFilterCtrl: FormControl = new FormControl<string>("");
|
||||
protected _onDestroy = new Subject<void>();
|
||||
|
||||
public campaignOnestart: any;
|
||||
public campaignOneend: any;
|
||||
rowDetail: GuiRowDetail = {
|
||||
enabled: true,
|
||||
template: (item) => {
|
||||
return `
|
||||
<div class='log-detail' style="width: 355px;color:#fff;background-color:${(() => {
|
||||
if (item.level == "Critical") return "#e55353";
|
||||
else if (item.level == "Warning") return "#f9b115";
|
||||
else item.level == "Info";
|
||||
return "#3399ff";
|
||||
})()}">
|
||||
<h1>Device :</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Device Name</td>
|
||||
<td>${item.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device IP</td>
|
||||
<td>${item.devip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device MAC</td>
|
||||
<td>${item.mac}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h1 style="margin-top: 10px;">Alert Detail :
|
||||
|
||||
</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Event</td>
|
||||
<td>${item.detail}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Event Status</td>
|
||||
<td><span (click)="logger(${item})" style="display:inline-block;background-color:${
|
||||
item.status ? "green" : "#db4848"
|
||||
} ;padding: 4px 10px;border-radius: 5px;line-height: 10px;color: rgba(255, 255, 255, 0.87);">${
|
||||
item.status ? "Fixed" : "Not Fixed"
|
||||
}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Event Category</td>
|
||||
<td>${item.eventtype}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exec time</td>
|
||||
<td>${item.eventtime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Detail</td>
|
||||
<td>${item.comment}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Source</td>
|
||||
<td>${item.src}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
getSeverityColor(level: string): string {
|
||||
if (level === "Critical") return "#e55353";
|
||||
if (level === "Warning") return "#f9b115";
|
||||
return "#3399ff";
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
showLogDetails(log: any) {
|
||||
this.selectedLog = log;
|
||||
this.detailsVisible = true;
|
||||
}
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
ngOnInit(): void {
|
||||
var _self = this;
|
||||
if (this.component_devid) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ import {
|
|||
import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
|
||||
import { DevLogsRoutingModule } from "./devlogs-routing.module";
|
||||
import { DevLogsComponent } from "./devlogs.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { DrawerModule } from 'primeng/drawer';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { MatDatepickerModule } from "@angular/material/datepicker";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
|
|
@ -34,7 +37,10 @@ import { MatSelectModule } from "@angular/material/select";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
DrawerModule,
|
||||
InputTextModule,
|
||||
TooltipModule,
|
||||
CollapseModule,
|
||||
BadgeModule,
|
||||
MatInputModule,
|
||||
|
|
|
|||
|
|
@ -8,22 +8,24 @@
|
|||
</c-col>
|
||||
<c-col xs [lg]="9">
|
||||
<h6 style="text-align: right;">
|
||||
<button cButton color="success" (click)="openAddDeviceModal()" class="mx-1"
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-plus"></i> Bulk Add </button>
|
||||
<button cButton color="success" (click)="openAddDeviceModal()" class="mx-1" size="sm"
|
||||
style="color: #fff;"><i class="fa-solid fa-plus"></i> Bulk Add </button>
|
||||
<button cButton color="dark" (click)="scanwizard(1,'')" [cModalToggle]="ScannerModal.id" class="mx-1"
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-magnifying-glass"></i> Scan</button>
|
||||
<button cButton color="primary" (click)="show_exec()" class="mx-1"
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-history"></i> History</button>
|
||||
<button cButton color="primary" (click)="show_exec()" class="mx-1" size="sm" style="color: #fff;"><i
|
||||
class="fa-solid fa-history"></i> History</button>
|
||||
</h6>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<c-row>
|
||||
<c-row>
|
||||
<c-col [lg]="9">
|
||||
<button cButton color="danger" class="mx-1" size="sm" style="color: #fff;" (click)="filterUpdatable()">{{updates.length}} Updatable
|
||||
<button cButton color="danger" class="mx-1" size="sm" style="color: #fff;"
|
||||
(click)="filterUpdatable()">{{updates.length}} Updatable
|
||||
</button>
|
||||
<button cButton color="warning" class="mx-1" size="sm" style="color: #fff;" (click)="filterUpgradable()">{{upgrades.length}}
|
||||
<button cButton color="warning" class="mx-1" size="sm" style="color: #fff;"
|
||||
(click)="filterUpgradable()">{{upgrades.length}}
|
||||
Upgradable</button>
|
||||
<button cButton color="secondary" class="mx-1" size="sm" (click)="clearFilter()">Clear Filter</button>
|
||||
</c-col>
|
||||
|
|
@ -38,116 +40,162 @@
|
|||
</c-input-group>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<gui-grid #grid [rowClass]="rowClass" [source]="source" [searching]="searching" [paging]="paging"
|
||||
[columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection"
|
||||
(selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<button cButton size="sm" variant="ghost" color="primary" (click)="webAccess(item)"
|
||||
style="padding: 2px 4px; margin-right: 5px; border: none;" cTooltip="Web Access">
|
||||
<i class="fas fa-globe" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
|
||||
<i *ngIf="item.status=='updated'" cTooltip="Tooltip text"
|
||||
style="color: green; margin-right: 3px;font-size: .7em;" class="fa-solid fa-check"></i>
|
||||
<i *ngIf="item.status=='failed'" cTooltip="Update failed"
|
||||
style="color: red; margin-right: 3px;font-size: .7em;" class="fa-solid fa-x"></i>
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="CPU Type" field="arch">
|
||||
<ng-template let-value="item.arch" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Firmware" field="current_firmware">
|
||||
<ng-template let-value="item.current_firmware" let-item="item" let-index="index">
|
||||
<div>{{value}}</div>
|
||||
<i *ngIf="item.update_availble" cTooltip="Firmware Update availble"
|
||||
class="fa-solid fa-up-long text-primary mx-1"></i>
|
||||
<i *ngIf="item.upgrade_availble" cTooltip="Device Firmware not Upgraded"
|
||||
class="fa-solid fa-microchip text-danger mx-1"></i>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="MAC Address" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="License" field="license" [enabled]="false">
|
||||
<ng-template let-value="item.license" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Interface" field="interface" [enabled]="false">
|
||||
<ng-template let-value="item.interface" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Created" field="created" [enabled]="false">
|
||||
<ng-template let-value="item.created" let-item="item.id" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Uptime" field="uptime">
|
||||
<ng-template let-value="item.uptime" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Created" field="created" [enabled]="false">
|
||||
<ng-template let-value="item.created" let-item="item.id" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column align="center" [cellEditing]="false" [sorting]="false" header="Action">
|
||||
<ng-template let-value="value" let-item="item">
|
||||
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary" (click)="show_detail(item)"
|
||||
style="border: none;padding: 4px 7px;"><i class="fa-regular fa-eye"></i><small> Details</small>
|
||||
</button>
|
||||
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
|
||||
[matMenuTriggerFor]="menu" cButton>
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<div cListGroup>
|
||||
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
|
||||
<button size="sm" (click)="single_device_action(item,'edit')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="fa-solid fa-pencil"></i><small>
|
||||
Edit Device</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'firmware')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-magnifying-glass"></i><small>
|
||||
Check Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'update')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-upload"></i><small>
|
||||
Update Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'upgrade')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-microchip"></i><small>
|
||||
Upgrade Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'devlogs')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="fa-regular fa-rectangle-list"></i><small>
|
||||
Device Logs</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'logauth')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-regular fa-clock"></i><small>
|
||||
Show Auth Logs</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'logacc')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-table-list"></i><small>
|
||||
Show Acc Logs</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'backup')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-success fa-solid fa-database"></i><small>
|
||||
Show Backups</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'delete')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
|
||||
Delete Device</small></button>
|
||||
<div class="mb-1 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search devices..."
|
||||
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[(selection)]="selected_rows" (selectionChange)="onSelectionChange($event)"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name','ip','mac','arch']">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 3rem" pResizableColumn>
|
||||
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</div>
|
||||
</mat-menu>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</th>
|
||||
<th pSortableColumn="arch" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>CPU Type</span>
|
||||
<span>
|
||||
<p-sortIcon field="arch"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="arch" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="current_firmware" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Current Firmware</span>
|
||||
<span>
|
||||
<p-sortIcon field="current_firmware"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="current_firmware" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="ip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>IP Address</span>
|
||||
<span>
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="mac" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>MAC Address</span>
|
||||
<span>
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
|
||||
<th pSortableColumn="uptime" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Uptime</span>
|
||||
<span>
|
||||
<p-sortIcon field="uptime"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="uptime" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr [ngClass]="item.status === 'updating' ? 'row-highlighted' : ''">
|
||||
<td>
|
||||
<p-tableCheckbox [value]="item"></p-tableCheckbox>
|
||||
</td>
|
||||
<td>
|
||||
<button cButton size="sm" variant="ghost" color="primary" (click)="webAccess(item)"
|
||||
style="padding: 2px 4px; margin-right: 5px; border: none;" cTooltip="Web Access">
|
||||
<i class="fas fa-globe" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
|
||||
<i *ngIf="item.status=='updated'" cTooltip="Updated"
|
||||
style="color: green; margin-right: 3px;font-size: .7em;" class="fa-solid fa-check"></i>
|
||||
<i *ngIf="item.status=='failed'" cTooltip="Update failed"
|
||||
style="color: red; margin-right: 3px;font-size: .7em;" class="fa-solid fa-x"></i>
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>{{item.arch}}</td>
|
||||
<td>
|
||||
<div style="display:inline-block">{{item.current_firmware}}</div>
|
||||
<i *ngIf="item.update_availble" cTooltip="Firmware Update available"
|
||||
class="fa-solid fa-up-long text-primary mx-1"></i>
|
||||
<i *ngIf="item.upgrade_availble" cTooltip="Device Firmware not Upgraded"
|
||||
class="fa-solid fa-microchip text-danger mx-1"></i>
|
||||
</td>
|
||||
<td>{{item.ip}}</td>
|
||||
<td>{{item.mac}}</td>
|
||||
<td>{{item.uptime}}</td>
|
||||
<td class="text-center">
|
||||
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary"
|
||||
(click)="show_detail(item)" style="border: none;padding: 4px 7px;"><i
|
||||
class="fa-regular fa-eye"></i><small> Details</small>
|
||||
</button>
|
||||
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
|
||||
[matMenuTriggerFor]="menu" cButton>
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<div cListGroup>
|
||||
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
|
||||
<button size="sm" (click)="single_device_action(item,'edit')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="fa-solid fa-pencil"></i><small>
|
||||
Edit Device</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'firmware')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-magnifying-glass"></i><small>
|
||||
Check Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'update')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-upload"></i><small>
|
||||
Update Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'upgrade')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-microchip"></i><small>
|
||||
Upgrade Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'devlogs')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="fa-regular fa-rectangle-list"></i><small>
|
||||
Device Logs</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'logauth')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-regular fa-clock"></i><small>
|
||||
Show Auth Logs</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'logacc')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-table-list"></i><small>
|
||||
Show Acc Logs</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'backup')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-success fa-solid fa-database"></i><small>
|
||||
Show Backups</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'delete')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
|
||||
Delete Device</small></button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="8">No devices found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<c-navbar *ngIf="rows.length!= 0" class="bg-light" colorScheme="light" expand="lg">
|
||||
<c-container [fluid]="true">
|
||||
<a cNavbarBrand href="javascript:;">
|
||||
|
|
@ -292,54 +340,105 @@
|
|||
</select>
|
||||
</c-input-group>
|
||||
</div>
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching"
|
||||
[source]="filteredExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [paging]="paging">
|
||||
<gui-grid-column header="Type" field="task_type" [width]="100">
|
||||
<ng-template let-value="item['task_type']" let-item="item" let-index="index">
|
||||
<i *ngIf="item.task_type === 'ip-scan'" class="fas fa-search" style="margin-right: 3px; color: #0d6efd;"></i>
|
||||
<i *ngIf="item.task_type === 'bulk-add'" class="fas fa-plus-circle" style="margin-right: 3px; color: #198754;"></i>
|
||||
<span style="font-size: 11px;">{{getTaskTypeLabel(value)}}</span>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Time" field="time" [width]="220">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<div style="font-size: 10px; line-height: 1.3;">
|
||||
<div><strong>Start:</strong> {{item.started}}</div>
|
||||
<div><strong>End:</strong> {{item.ended}}</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Details" field="details">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<div *ngIf="item.task_type === 'ip-scan'" style="font-size: 12px; line-height: 1.3;">
|
||||
<div>{{item.start_ip}} - {{item.end_ip}}</div>
|
||||
<div>User: {{item.username}}</div>
|
||||
</div>
|
||||
<div *ngIf="item.task_type === 'bulk-add'" style="font-size: 12px; line-height: 1.3;">
|
||||
<div>{{item.device_count}} devices</div>
|
||||
<div style="word-break: break-all;">{{item.task_id}}</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Results" field="results" align="center" [width]="70">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<div style="font-size: 11px;">
|
||||
<span style="color: green;">✓ {{item.success_count}}</span>
|
||||
<span *ngIf="item.failed_count > 0" style="color: red; margin-left: 5px;">✗ {{item.failed_count}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" field="actions" align="center" [width]="160">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<button (click)="showTaskDetails(item)" color="info" size="sm" cButton class="me-1">
|
||||
<i class="fas fa-eye"></i> Details
|
||||
</button>
|
||||
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton>
|
||||
<i class="fas fa-download"></i> CSV
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search history..."
|
||||
(input)="dtTask.filterGlobal($any($event.target).value, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtTask [value]="filteredExecutedData" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['task_id', 'username', 'start_ip', 'end_ip']">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>#No</span>
|
||||
<p-sortIcon field="index"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="createdC" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Execution Time</span>
|
||||
<span>
|
||||
<p-sortIcon field="createdC"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="createdC" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="username" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>User</span>
|
||||
<span>
|
||||
<p-sortIcon field="username"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 100px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<i *ngIf="item.task_type === 'ip-scan'" class="fas fa-search text-primary"></i>
|
||||
<i *ngIf="item.task_type === 'bulk-add'" class="fas fa-plus-circle text-success"></i>
|
||||
<span class="small">{{getTaskTypeLabel(item.task_type)}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="small">
|
||||
<div><strong>S:</strong> {{item.started}}</div>
|
||||
<div><strong>E:</strong> {{item.ended}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div *ngIf="item.task_type === 'ip-scan'" class="small">
|
||||
<div>{{item.start_ip}} - {{item.end_ip}}</div>
|
||||
<div class="text-muted">User: {{item.username}}</div>
|
||||
</div>
|
||||
<div *ngIf="item.task_type === 'bulk-add'" class="small">
|
||||
<div>{{item.device_count}} devices</div>
|
||||
<div class="text-muted text-truncate" style="max-width: 150px;">{{item.task_id}}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="text-success small">✓ {{item.success_count}}</span>
|
||||
<span *ngIf="item.failed_count > 0" class="text-danger small">✗ {{item.failed_count}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button (click)="showTaskDetails(item)" color="info" size="sm" cButton class="me-1"
|
||||
style="padding: 2px 6px;">
|
||||
<i class="fas fa-eye"></i> Details
|
||||
</button>
|
||||
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton style="padding: 2px 6px;">
|
||||
<i class="fas fa-download"></i> CSV
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center p-4">No task history found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="ExecutedDataModalVisible=!ExecutedDataModalVisible" cButton color="secondary">
|
||||
|
|
@ -453,7 +552,8 @@
|
|||
<c-modal-body>
|
||||
<div *ngIf="addDeviceStep === 1">
|
||||
<h6>Upload CSV File</h6>
|
||||
<p>Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port</p>
|
||||
<p>Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port
|
||||
</p>
|
||||
<input type="file" accept=".csv" (change)="onFileSelected($event)" class="form-control mb-3">
|
||||
<div *ngIf="csvPreview.length > 0">
|
||||
<h6>Preview (First 3 rows):</h6>
|
||||
|
|
@ -522,7 +622,8 @@
|
|||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="addDeviceStep === 1" cButton color="primary" (click)="uploadDevices()" [disabled]="!isValidMapping()">
|
||||
<button *ngIf="addDeviceStep === 1" cButton color="primary" (click)="uploadDevices()"
|
||||
[disabled]="!isValidMapping()">
|
||||
Add Devices
|
||||
</button>
|
||||
<button *ngIf="addDeviceStep === 3" cButton color="success" (click)="closeAddDeviceModal()">
|
||||
|
|
@ -557,13 +658,14 @@
|
|||
</c-modal>
|
||||
|
||||
<!-- Task Details Modal -->
|
||||
<c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal"
|
||||
style="z-index: 1060; backdrop-filter: blur(2px);">
|
||||
<c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal"
|
||||
style="z-index: 1060; backdrop-filter: blur(2px);">
|
||||
<c-modal-header style="background: rgba(255,255,255,0.95); backdrop-filter: blur(10px);">
|
||||
<h5 cModalTitle>{{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results</h5>
|
||||
<button (click)="closeDetailsModal()" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body *ngIf="selectedTaskDetails" style="background: rgba(255,255,255,0.98); backdrop-filter: blur(10px); max-height: 70vh; overflow-y: auto;">
|
||||
<c-modal-body *ngIf="selectedTaskDetails"
|
||||
style="background: rgba(255,255,255,0.98); backdrop-filter: blur(10px); max-height: 70vh; overflow-y: auto;">
|
||||
<div class="mb-3">
|
||||
<c-row>
|
||||
<c-col md="6">
|
||||
|
|
@ -587,8 +689,8 @@
|
|||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0">Detailed Results:</h6>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" placeholder="Search IP or error..."
|
||||
[(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()">
|
||||
<input type="text" class="form-control form-control-sm" placeholder="Search IP or error..."
|
||||
[(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()">
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-hover table-responsive">
|
||||
|
|
@ -620,19 +722,20 @@
|
|||
<nav *ngIf="getTotalDetailsPages() > 1" class="mt-3">
|
||||
<ul class="pagination pagination-sm justify-content-center">
|
||||
<li class="page-item" [class.disabled]="detailsCurrentPage === 1">
|
||||
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage - 1)" [disabled]="detailsCurrentPage === 1">
|
||||
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage - 1)"
|
||||
[disabled]="detailsCurrentPage === 1">
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
<li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index"
|
||||
[class.active]="detailsCurrentPage === i + 1">
|
||||
<li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index"
|
||||
[class.active]="detailsCurrentPage === i + 1">
|
||||
<button class="page-link" (click)="onDetailsPageChange(i + 1)">
|
||||
{{i + 1}}
|
||||
</button>
|
||||
</li>
|
||||
<li class="page-item" [class.disabled]="detailsCurrentPage === getTotalDetailsPages()">
|
||||
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)"
|
||||
[disabled]="detailsCurrentPage === getTotalDetailsPages()">
|
||||
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)"
|
||||
[disabled]="detailsCurrentPage === getTotalDetailsPages()">
|
||||
Next
|
||||
</button>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -10,21 +10,9 @@ import { dataProvider } from "../../providers/mikrowizard/data";
|
|||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiGridComponent,
|
||||
GuiGridApi,
|
||||
GuiRowClass,
|
||||
GuiSearching,
|
||||
GuiSelectedRow,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
ToasterComponent
|
||||
} from "@coreui/angular";
|
||||
import { Table } from 'primeng/table';
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
|
|
@ -34,9 +22,9 @@ import { formatInTimeZone } from "date-fns-tz";
|
|||
templateUrl: "devices.component.html",
|
||||
})
|
||||
export class DevicesComponent implements OnInit, OnDestroy {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public tz: string;
|
||||
public uid!: number;
|
||||
public uname!: string;
|
||||
public tz!: string;
|
||||
public ispro:boolean=false;
|
||||
|
||||
constructor(
|
||||
|
|
@ -69,14 +57,11 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
return value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
}
|
||||
@ViewChild("grid", { static: true }) gridComponent: GuiGridComponent;
|
||||
@ViewChild("dt") table!: Table;
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
public source: Array<any> = [];
|
||||
public originalSource: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
public upgrades: any = [];
|
||||
public updates: any = [];
|
||||
public scanwizard_step: number = 1;
|
||||
|
|
@ -117,6 +102,10 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
public uploadResult = { success: 0, failed: 0, resultFile: null };
|
||||
public currentTaskId: string = '';
|
||||
public statusCheckTimer: any;
|
||||
|
||||
public selected_rows: any[] = []; // Used by p-table selection
|
||||
public Selectedrows: any[] = []; // Legacy ID array used by actions
|
||||
public rows: any = []; // For legacy internal use
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
|
|
@ -125,7 +114,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
fade: true,
|
||||
closeButton: true,
|
||||
};
|
||||
rowClass: GuiRowClass = {
|
||||
rowClass: any = {
|
||||
class: "row-highlighted",
|
||||
};
|
||||
public sorting = {
|
||||
|
|
@ -134,38 +123,36 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
};
|
||||
public ip_scanner: any;
|
||||
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
public paging: any = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
public columnMenu: any = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
public infoPanel: any = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
|
||||
onSelectionChange(value: any[]) {
|
||||
this.selected_rows = value;
|
||||
this.Selectedrows = value.map(item => item.id);
|
||||
this.rows = value; // For legacy compat if needed
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.selected_group = Number(this.route.snapshot.paramMap.get("id"));
|
||||
this.initGridTable();
|
||||
|
|
@ -177,8 +164,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
single_device_action(dev: any, action: string) {
|
||||
const api: GuiGridApi = this.gridComponent.api;
|
||||
api.unselectAll();
|
||||
this.selected_rows = [];
|
||||
this.Selectedrows = [dev["id"]];
|
||||
switch (action) {
|
||||
case "edit":
|
||||
|
|
@ -273,10 +259,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
onSelectedRows(rows: Array<GuiSelectedRow>): void {
|
||||
this.rows = rows;
|
||||
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
}
|
||||
// Removed legacy onSelectedRows
|
||||
|
||||
checkvalid(type: string): boolean {
|
||||
var rx =
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ import {
|
|||
import { MatMenuModule } from "@angular/material/menu";
|
||||
import { DevicesRoutingModule } from "./devices-routing.module";
|
||||
import { DevicesComponent } from "./devices.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PrimeNGTableModule } from 'primeng/table';
|
||||
import { CheckboxModule } from 'primeng/checkbox';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { MultiSelectModule } from 'primeng/multiselect';
|
||||
import { TooltipModule as PrimeNGTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -34,7 +38,6 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
NavbarModule,
|
||||
CollapseModule,
|
||||
DropdownModule,
|
||||
|
|
@ -46,6 +49,11 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
MatMenuModule,
|
||||
TooltipModule,
|
||||
TableModule,
|
||||
PrimeNGTableModule,
|
||||
CheckboxModule,
|
||||
InputTextModule,
|
||||
MultiSelectModule,
|
||||
PrimeNGTooltipModule
|
||||
],
|
||||
declarations: [DevicesComponent],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,82 +13,119 @@
|
|||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<gui-grid [source]="source" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[autoResizeWidth]=true>
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Devices" field="array_agg" align="CENTER">
|
||||
<ng-template let-value="item.array_agg" let-item="item" let-index="index">
|
||||
<ng-container *ngIf="item.id==1 ; then Default;else NotDefault">
|
||||
</ng-container>
|
||||
<ng-template #Default>
|
||||
<c-badge color="info"><i class="fa-solid fa-network-wired me-1"></i>All Devices</c-badge>
|
||||
</ng-template>
|
||||
<ng-template #NotDefault>
|
||||
<c-badge color="info" *ngIf="value[0]==null && item.id!=1"
|
||||
[cTooltip]="'No devices assigned'" style="cursor: help">
|
||||
<i class="fa-solid fa-server me-1"></i>0
|
||||
</c-badge>
|
||||
<c-badge color="info" *ngIf="value[0]!=null"
|
||||
[cTooltip]="getDevicesTooltip(item)"
|
||||
[cTooltipPlacement]="'top'" style="cursor: help">
|
||||
<i class="fa-solid fa-server me-1"></i>{{value.length}}
|
||||
</c-badge>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Create Time" field="created">
|
||||
<ng-template let-value="item.created" let-item="item" let-index="index">
|
||||
{{formatCreateTime(value)}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<div class="mb-1 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search groups..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gui-grid-column header="Users" field="assigned_users" width="80" align="CENTER">
|
||||
<ng-template let-value="item.assigned_users" let-item="item" let-index="index">
|
||||
<c-badge color="info"
|
||||
[cTooltip]="getUsersTooltip(value)"
|
||||
[cTooltipPlacement]="'top'"
|
||||
style="cursor: help">
|
||||
<i class="fa-solid fa-users me-1"></i>{{value.length}}
|
||||
</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name']" [loading]="loading">
|
||||
|
||||
<gui-grid-column align="center" [cellEditing]="false" [sorting]="false" header="Action">
|
||||
<ng-template let-value="value" let-item="item">
|
||||
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary" (click)="show_members(item.id)"
|
||||
style="border: none;padding: 4px 7px;"><i class="fa-regular fa-eye"></i><small> View devices</small>
|
||||
</button>
|
||||
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
|
||||
[matMenuTriggerFor]="menu" cButton>
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<div cListGroup>
|
||||
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
|
||||
<button size="sm" (click)="editAddGroup(item,'showedit')" style="padding: 4px 7px;"
|
||||
[disabled]="item.id==1" cListGroupItem><i class="fa-solid fa-pencil"></i><small>
|
||||
Edit Group</small></button>
|
||||
<button size="sm" (click)="manageUsers(item)" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="fa-solid fa-users-gear"></i><small>
|
||||
Manage Users</small></button>
|
||||
<button size="sm" (click)="groupFirmwareAction(item, 'update')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-success fa-solid fa-upload"></i><small>
|
||||
Update Firmware</small></button>
|
||||
<button size="sm" (click)="groupFirmwareAction(item, 'upgrade')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-secondary fa-solid fa-microchip"></i><small>
|
||||
Upgrade Firmware</small></button>
|
||||
<button size="sm" (click)="show_delete_group(item)" style="padding: 4px 7px;"
|
||||
[disabled]="item.id==1" cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
|
||||
Delete Group</small></button>
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</th>
|
||||
<th pSortableColumn="description" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Member Devices</span>
|
||||
<span>
|
||||
<p-sortIcon field="description"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="pair_type" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Created</span>
|
||||
<span>
|
||||
<p-sortIcon field="pair_type"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="pair_type" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="member_count" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Member Users</span>
|
||||
<span>
|
||||
<p-sortIcon field="member_count"></p-sortIcon>
|
||||
<p-columnFilter type="numeric" field="member_count" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><strong>{{item.name}}</strong></td>
|
||||
<td class="text-center">
|
||||
<ng-container *ngIf="item.id==1 ; then Default; else NotDefault"></ng-container>
|
||||
<ng-template #Default>
|
||||
<h6><c-badge color="info"><i class="fa-solid fa-network-wired me-1"></i>All Devices</c-badge></h6>
|
||||
</ng-template>
|
||||
<ng-template #NotDefault>
|
||||
<c-badge color="info" *ngIf="(!item.array_agg || item.array_agg[0]==null) && item.id!=1"
|
||||
pTooltip="No devices assigned" style="cursor: help">
|
||||
<i class="fa-solid fa-server me-1"></i>0
|
||||
</c-badge>
|
||||
<h6><c-badge color="info" *ngIf="item.array_agg && item.array_agg[0]!=null"
|
||||
[pTooltip]="getDevicesTooltip(item)" style="cursor: help">
|
||||
<i class="fa-solid fa-server me-1"></i>{{item.array_agg.length}}
|
||||
</c-badge></h6>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>{{formatCreateTime(item.created)}}</td>
|
||||
<td class="text-center">
|
||||
<h6><c-badge color="info" [pTooltip]="getUsersTooltip(item.assigned_users)" style="cursor: help">
|
||||
<i class="fa-solid fa-users me-1"></i>{{item.assigned_users?.length || 0}}
|
||||
</c-badge></h6>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex justify-content-center">
|
||||
<button size="sm" variant="outline" cButton color="primary" (click)="show_members(item.id)"
|
||||
style="border: none;padding: 4px 7px;" pTooltip="View devices"><i class="fa-regular fa-eye"></i>
|
||||
</button>
|
||||
<button color="primary" variant="ghost" style="padding: 4px 7px;" [matMenuTriggerFor]="menu" cButton>
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<div cListGroup>
|
||||
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
|
||||
<button size="sm" (click)="editAddGroup(item,'showedit')" style="padding: 4px 7px;"
|
||||
[disabled]="item.id==1" cListGroupItem><i class="fa-solid fa-pencil"></i><small>
|
||||
Edit Group</small></button>
|
||||
<button size="sm" (click)="manageUsers(item)" style="padding: 4px 7px;" cListGroupItem><i
|
||||
class="fa-solid fa-users-gear"></i><small>
|
||||
Manage Users</small></button>
|
||||
<button size="sm" (click)="groupFirmwareAction(item, 'update')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-success fa-solid fa-upload"></i><small>
|
||||
Update Firmware</small></button>
|
||||
<button size="sm" (click)="groupFirmwareAction(item, 'upgrade')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-secondary fa-solid fa-microchip"></i><small>
|
||||
Upgrade Firmware</small></button>
|
||||
<button size="sm" (click)="show_delete_group(item)" style="padding: 4px 7px;"
|
||||
[disabled]="item.id==1" cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
|
||||
Delete Group</small></button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
|
@ -134,30 +171,54 @@
|
|||
<p class="mb-0">Click "Add Devices" to start adding devices to this group</p>
|
||||
</div>
|
||||
<div *ngIf="groupMembers.length > 0">
|
||||
<gui-grid [autoResizeWidth]="true" [searching]="searching" [source]="groupMembers" [columnMenu]="columnMenu"
|
||||
[sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection"
|
||||
(selectedRows)="onSelectedRowsMembers($event)" [paging]="paging">
|
||||
<gui-grid-column header="Device Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid fa-server me-2 text-primary"></i>
|
||||
<strong>{{value}}</strong>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
<c-badge color="secondary">{{value}}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="100" field="action" align="CENTER">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="remove_from_group(item.id)" title="Remove from group">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search members..."
|
||||
(input)="applyFilterMembers($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtMembers [value]="groupMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm" [(selection)]="MemberRows"
|
||||
(selectionChange)="onSelectedRowsMembers($event)" [globalFilterFields]="['name', 'ip']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 4rem"><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Name</span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="mac" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>MAC Address</span>
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 100px" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid fa-server me-2 text-primary"></i>
|
||||
<strong>{{item.name}}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td><c-badge color="secondary">{{item.ip}}</c-badge></td>
|
||||
<td class="text-center">
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="remove_from_group(item.id)"
|
||||
pTooltip="Remove from group">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
|
@ -172,7 +233,8 @@
|
|||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-modal #NewMemberModal [backdrop]="true" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal" style="z-index: 1060; backdrop-filter: blur(2px);">
|
||||
<c-modal #NewMemberModal [backdrop]="true" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal"
|
||||
style="z-index: 1060; backdrop-filter: blur(2px);">
|
||||
<c-modal-header class="bg-success text-white">
|
||||
<h5 cModalTitle><i class="fa-solid fa-plus-circle me-2"></i>Add Devices to Group</h5>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
|
||||
|
|
@ -189,7 +251,8 @@
|
|||
<!-- Available Devices -->
|
||||
<c-card>
|
||||
<c-card-header>
|
||||
<h6 class="mb-0"><i class="fa-solid fa-server me-2"></i>Available Devices ({{availbleMembers.length}} total)</h6>
|
||||
<h6 class="mb-0"><i class="fa-solid fa-server me-2"></i>Available Devices ({{availbleMembers.length}} total)
|
||||
</h6>
|
||||
</c-card-header>
|
||||
<c-card-body class="p-0">
|
||||
<div *ngIf="availbleMembers.length === 0" class="text-center p-4 text-muted">
|
||||
|
|
@ -198,28 +261,45 @@
|
|||
<p class="mb-0">No available devices to add to this group</p>
|
||||
</div>
|
||||
<div *ngIf="availbleMembers.length > 0">
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
|
||||
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging">
|
||||
<gui-grid-column header="Device Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid fa-server me-2 text-primary"></i>
|
||||
<strong>{{value}}</strong>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
<c-badge color="secondary">{{value}}</c-badge>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="MAC Address" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
<small class="text-muted">{{value}}</small>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end w-100">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search available..."
|
||||
(input)="applyFilterNewMembers($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [resizableColumns]="true"
|
||||
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped" [(selection)]="NewMemberRows"
|
||||
(selectionChange)="onSelectedRowsNewMembers($event)" [globalFilterFields]="['name', 'ip', 'mac']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
|
||||
<th pSortableColumn="name" pResizableColumn>Device Name <p-sortIcon
|
||||
field="name"></p-sortIcon><p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="ip" pResizableColumn>IP Address <p-sortIcon field="ip"></p-sortIcon><p-columnFilter
|
||||
type="text" field="ip" display="menu" class="ms-auto" /></th>
|
||||
<th pSortableColumn="mac" pResizableColumn>MAC Address <p-sortIcon
|
||||
field="mac"></p-sortIcon><p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-solid fa-server me-2 text-primary"></i>
|
||||
<strong>{{item.name}}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td><c-badge color="secondary">{{item.ip}}</c-badge></td>
|
||||
<td><small class="text-muted">{{item.mac}}</small></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
|
@ -268,7 +348,8 @@
|
|||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
<c-modal #UserManagementModal backdrop="static" size="xl" [(visible)]="UserManagementModalVisible" id="UserManagementModal">
|
||||
<c-modal #UserManagementModal backdrop="static" size="xl" [(visible)]="UserManagementModalVisible"
|
||||
id="UserManagementModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle><i class="fa-solid fa-users-gear me-2"></i>User Permissions - {{selectedGroup?.name}}</h5>
|
||||
<button [cModalToggle]="UserManagementModal.id" cButtonClose></button>
|
||||
|
|
@ -283,18 +364,11 @@
|
|||
<c-col md="4">
|
||||
<div class="search-select-wrapper">
|
||||
<label class="search-label">User</label>
|
||||
<input cFormControl
|
||||
[(ngModel)]="userSearch"
|
||||
(input)="filterUsers($event)"
|
||||
(focus)="showUserDropdown = true"
|
||||
(blur)="hideUserDropdown()"
|
||||
placeholder="Search and select user..."
|
||||
class="search-input compact-select"
|
||||
autocomplete="off" />
|
||||
<input cFormControl [(ngModel)]="userSearch" (input)="filterUsers($event)"
|
||||
(focus)="showUserDropdown = true" (blur)="hideUserDropdown()" placeholder="Search and select user..."
|
||||
class="search-input compact-select" autocomplete="off" />
|
||||
<div *ngIf="showUserDropdown && filteredUsers.length > 0" class="search-dropdown">
|
||||
<div *ngFor="let user of filteredUsers"
|
||||
class="search-option"
|
||||
(mousedown)="selectUser(user)">
|
||||
<div *ngFor="let user of filteredUsers" class="search-option" (mousedown)="selectUser(user)">
|
||||
{{user.username}} ({{user.first_name}} {{user.last_name}})
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -306,28 +380,24 @@
|
|||
<c-col md="4">
|
||||
<div class="search-select-wrapper">
|
||||
<label class="search-label">Permission</label>
|
||||
<input cFormControl
|
||||
[(ngModel)]="permissionSearch"
|
||||
(input)="filterPermissions($event)"
|
||||
(focus)="showPermissionDropdown = true"
|
||||
(blur)="hidePermissionDropdown()"
|
||||
placeholder="Search and select permission..."
|
||||
class="search-input compact-select"
|
||||
autocomplete="off" />
|
||||
<input cFormControl [(ngModel)]="permissionSearch" (input)="filterPermissions($event)"
|
||||
(focus)="showPermissionDropdown = true" (blur)="hidePermissionDropdown()"
|
||||
placeholder="Search and select permission..." class="search-input compact-select" autocomplete="off" />
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
|
||||
<div *ngFor="let perm of filteredPermissions"
|
||||
class="search-option"
|
||||
<div *ngFor="let perm of filteredPermissions" class="search-option"
|
||||
(mousedown)="selectPermission(perm)">
|
||||
{{perm.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch" class="search-no-results">
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch"
|
||||
class="search-no-results">
|
||||
No permissions found
|
||||
</div>
|
||||
</div>
|
||||
</c-col>
|
||||
<c-col md="4" class="d-flex align-items-end">
|
||||
<button cButton color="success" (click)="addUserPermission()" [disabled]="!selectedUser || !selectedPermission">
|
||||
<button cButton color="success" (click)="addUserPermission()"
|
||||
[disabled]="!selectedUser || !selectedPermission">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Permission
|
||||
</button>
|
||||
</c-col>
|
||||
|
|
@ -336,7 +406,8 @@
|
|||
</c-card>
|
||||
<c-card>
|
||||
<c-card-header class="bg-light">
|
||||
<h6 class="mb-0"><i class="fa-solid fa-list-check me-2"></i>Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)</h6>
|
||||
<h6 class="mb-0"><i class="fa-solid fa-list-check me-2"></i>Current Permissions
|
||||
({{selectedGroup?.assigned_users?.length || 0}} users)</h6>
|
||||
</c-card-header>
|
||||
<c-card-body class="p-0">
|
||||
<div *ngIf="selectedGroup?.assigned_users?.length === 0" class="text-center p-4 text-muted">
|
||||
|
|
@ -369,14 +440,12 @@
|
|||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<button cButton color="warning" size="sm" variant="outline"
|
||||
(click)="editUserPermission(user)" title="Change Permission"
|
||||
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
|
||||
<button cButton color="warning" size="sm" variant="outline" (click)="editUserPermission(user)"
|
||||
title="Change Permission" [disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
|
||||
<i class="fa-solid fa-edit"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" variant="outline"
|
||||
(click)="removeUserPermission(user)" title="Remove Permission"
|
||||
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="removeUserPermission(user)"
|
||||
title="Remove Permission" [disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -402,7 +471,8 @@
|
|||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Current Permission: <c-badge [color]="getPermissionColor(editingUser?.perm_name)">{{editingUser?.perm_name}}</c-badge></label>
|
||||
<label class="form-label">Current Permission: <c-badge
|
||||
[color]="getPermissionColor(editingUser?.perm_name)">{{editingUser?.perm_name}}</c-badge></label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">New Permission</label>
|
||||
|
|
@ -430,7 +500,9 @@
|
|||
<c-modal-body>
|
||||
<div class="text-center">
|
||||
<i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i>
|
||||
<p>Are you sure you want to remove <strong>{{removingUser?.username}}</strong>'s permission from group <strong>{{selectedGroup?.name}}</strong>?</p>
|
||||
<p>Are you sure you want to remove <strong>{{removingUser?.username}}</strong>'s permission from group
|
||||
<strong>{{selectedGroup?.name}}</strong>?
|
||||
</p>
|
||||
<p class="text-muted small">This action cannot be undone.</p>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
|
|
@ -451,13 +523,17 @@
|
|||
<c-modal-body>
|
||||
<div class="text-center">
|
||||
<i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i>
|
||||
<p>Are you sure you want to <strong>{{firmwareAction}}</strong> firmware for all devices in group <strong>{{selectedGroupForFirmware?.name}}</strong>?</p>
|
||||
<p class="text-muted small">This action will affect all devices in this group and may take some time to complete.</p>
|
||||
<p>Are you sure you want to <strong>{{firmwareAction}}</strong> firmware for all devices in group
|
||||
<strong>{{selectedGroupForFirmware?.name}}</strong>?
|
||||
</p>
|
||||
<p class="text-muted small">This action will affect all devices in this group and may take some time to complete.
|
||||
</p>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="primary" (click)="confirmGroupFirmwareAction()">
|
||||
<i class="fa-solid fa-{{firmwareAction === 'update' ? 'upload' : 'microchip'}} me-1"></i>{{firmwareAction | titlecase}} Firmware
|
||||
<i class="fa-solid fa-{{firmwareAction === 'update' ? 'upload' : 'microchip'}} me-1"></i>{{firmwareAction |
|
||||
titlecase}} Firmware
|
||||
</button>
|
||||
<button [cModalToggle]="FirmwareConfirmModal.id" cButton color="secondary">
|
||||
Cancel
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import {
|
||||
GuiSearching,
|
||||
GuiSelectedRow,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
interface IUser {
|
||||
name: string;
|
||||
|
|
@ -35,9 +24,13 @@ interface IUser {
|
|||
styleUrls: ["devgroup.component.scss"]
|
||||
})
|
||||
export class DevicesGroupComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public tz: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public tz: string = '';
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
@ViewChild('dtMembers') tableMembers!: Table;
|
||||
@ViewChild('dtNewMembers') tableNewMembers!: Table;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
|
|
@ -68,7 +61,6 @@ export class DevicesGroupComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public MemberRows: any = [];
|
||||
public NewMemberRows: any = [];
|
||||
|
|
@ -117,42 +109,17 @@ export class DevicesGroupComponent implements OnInit {
|
|||
id: 0,
|
||||
name: "",
|
||||
};
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
applyFilterMembers($event: any, stringVal: string) {
|
||||
this.tableMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterNewMembers($event: any, stringVal: string) {
|
||||
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGridTable();
|
||||
|
|
@ -175,13 +142,13 @@ export class DevicesGroupComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onSelectedRowsMembers(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRowsMembers(rows: any[]): void {
|
||||
this.MemberRows = rows;
|
||||
this.SelectedMemberRows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
this.SelectedMemberRows = rows.map((m: any) => m.id);
|
||||
}
|
||||
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRowsNewMembers(rows: any[]): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
this.SelectedNewMemberRows = rows.map((m: any) => m.id);
|
||||
}
|
||||
add_new_members() {
|
||||
var _self = this;
|
||||
|
|
@ -193,7 +160,7 @@ export class DevicesGroupComponent implements OnInit {
|
|||
this.groupMembers = [
|
||||
...new Set(
|
||||
this.groupMembers.concat(
|
||||
this.NewMemberRows.map((m: GuiSelectedRow) => m.source)
|
||||
this.NewMemberRows
|
||||
)
|
||||
),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import {
|
|||
} from "@coreui/angular";
|
||||
import { DevicesGroupRoutingModule } from "./devgroup-routing.module";
|
||||
import { DevicesGroupComponent } from "./devgroup.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
import { BadgeModule } from "@coreui/angular";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { MatMenuModule } from "@angular/material/menu";
|
||||
|
|
@ -30,7 +32,9 @@ import { MatMenuModule } from "@angular/material/menu";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
CollapseModule,
|
||||
ModalModule,
|
||||
BadgeModule,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
<div style="position: relative;">
|
||||
<c-row class="network-container">
|
||||
<c-col [xs]="12" class="network-col">
|
||||
<c-card class="network-card">
|
||||
<button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
<div #network class="network-canvas"></div>
|
||||
<div class="map-actions">
|
||||
<button cButton color="primary" size="sm" (click)="refreshData()" class="refresh-btn">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
<button cButton color="warning" size="sm" (click)="resetLocations()" class="refresh-btn">
|
||||
<i class="fas fa-thumbtack"></i> Reset Locations
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="showResetModal = true" class="refresh-btn">
|
||||
<i class="fas fa-trash-alt"></i> Reset Map
|
||||
</button>
|
||||
</div>
|
||||
<div class="network-canvas">
|
||||
<div #network class="full-size"></div>
|
||||
<div *ngIf="loadingMap && mikrotikData.length === 0" class="map-loading-overlay">
|
||||
<div class="loader-content">
|
||||
<i class="fas fa-project-diagram fa-spin"></i>
|
||||
<h3>Generating Map...</h3>
|
||||
<p>Please wait while we discover your network topology. This might take a few minutes. (Polling every 30s)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
|
|
@ -80,3 +98,21 @@
|
|||
<button cButton color="secondary" (click)="closeWebAccessModal()">Cancel</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Reset Map Confirmation Modal -->
|
||||
<c-modal #ResetMapModal backdrop="static" [(visible)]="showResetModal" id="ResetMapModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Confirm Map Reset</h6>
|
||||
<button cButtonClose (click)="showResetModal = false"></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p>Are you sure you want to reset all network discovery data?</p>
|
||||
<p class="text-danger"><i class="fas fa-exclamation-triangle"></i> This will delete all current neighbors' data and trigger a fresh scan. Node positions will also be reset.</p>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="secondary" (click)="showResetModal = false">Cancel</button>
|
||||
<button cButton color="danger" (click)="confirmResetMap()">Reset Everything</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,24 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
.map-actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
right: 15px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.network-canvas {
|
||||
|
|
@ -34,7 +45,49 @@
|
|||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.full-size {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.loader-content {
|
||||
i {
|
||||
font-size: 4rem;
|
||||
color: #3498db;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #7f8c8d;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .vis-network {
|
||||
cursor: default;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { DataSet } from 'vis-data';
|
|||
templateUrl: "maps.component.html",
|
||||
styleUrls: ["maps.component.scss"],
|
||||
})
|
||||
export class MapsComponent implements OnInit {
|
||||
export class MapsComponent implements OnInit, OnDestroy {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public ispro: boolean = false;
|
||||
|
|
@ -19,8 +19,12 @@ export class MapsComponent implements OnInit {
|
|||
public savedPositionsKey = "network-layout";
|
||||
public selectedDevice: any = null;
|
||||
public showWebAccessModal: boolean = false;
|
||||
public showMoreInfoModal: boolean = false;
|
||||
public currentDeviceInfo: any = null;
|
||||
public showResetModal: boolean = false;
|
||||
public loadingMap: boolean = false;
|
||||
private pollingTimer: any;
|
||||
private isDestroyed: boolean = false;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
private router: Router,
|
||||
|
|
@ -54,13 +58,38 @@ export class MapsComponent implements OnInit {
|
|||
this.loadNetworkData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
if (this.pollingTimer) {
|
||||
clearTimeout(this.pollingTimer);
|
||||
}
|
||||
}
|
||||
|
||||
loadNetworkData(): void {
|
||||
if (this.isDestroyed) return;
|
||||
clearTimeout(this.pollingTimer);
|
||||
this.loadingMap = true;
|
||||
this.data_provider.getNetworkMap().then((res) => {
|
||||
this.mikrotikData = res;
|
||||
console.dir(res);
|
||||
setTimeout(() => {
|
||||
this.createNetworkMap();
|
||||
}, 100);
|
||||
if (this.isDestroyed) return;
|
||||
// Normalize response - handle array or object with 'result' property
|
||||
const data = (res && res.result) ? res.result : res;
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
this.loadingMap = false;
|
||||
this.mikrotikData = data;
|
||||
setTimeout(() => {
|
||||
this.createNetworkMap();
|
||||
}, 100);
|
||||
} else {
|
||||
console.log("Map data is empty, likely generating. Retrying in 30s...");
|
||||
this.mikrotikData = [];
|
||||
this.pollingTimer = setTimeout(() => {
|
||||
this.loadNetworkData();
|
||||
}, 30000);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Error loading network map:", err);
|
||||
this.loadingMap = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -476,4 +505,31 @@ Object.entries(connectionMap).forEach(([connectionKey, interfacePairs]) => {
|
|||
// Implement configuration interface
|
||||
}
|
||||
|
||||
resetLocations() {
|
||||
localStorage.removeItem(this.savedPositionsKey);
|
||||
this.savedPositions = {};
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
confirmResetMap() {
|
||||
this.showResetModal = false;
|
||||
this.loadingMap = true;
|
||||
this.mikrotikData = [];
|
||||
this.selectedDevice = null;
|
||||
|
||||
this.data_provider.resetNetworkMap().then((res) => {
|
||||
if (res.status === 'success') {
|
||||
localStorage.removeItem(this.savedPositionsKey);
|
||||
this.savedPositions = {};
|
||||
this.loadNetworkData();
|
||||
} else {
|
||||
this.loadingMap = false;
|
||||
alert("Error: " + (res.error || "Failed to reset map"));
|
||||
}
|
||||
}).catch(err => {
|
||||
this.loadingMap = false;
|
||||
alert("Error resetting map: " + err);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import { MapsRoutingModule } from "./maps-routing.module";
|
|||
import { MapsComponent } from "./maps.component";
|
||||
import { ClipboardModule } from "@angular/cdk/clipboard";
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -53,7 +54,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
|||
TooltipModule,
|
||||
UtilitiesModule,
|
||||
InfiniteScrollModule,
|
||||
IconModule
|
||||
IconModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [MapsComponent],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<div style="position: relative;">
|
||||
<c-row style="height: calc(100vh - 10rem);" (click)="disableContextMenu()" >
|
||||
<c-col xs="3" style="height:100%;">
|
||||
<c-card style="height:100%">
|
||||
|
|
@ -176,6 +177,8 @@
|
|||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
<div *ngIf="contextmenu">
|
||||
<div class="contextmenu" [ngStyle]="{'left.px': contextmenuX, 'top.px': contextmenuY}">
|
||||
<c-card style="padding: 1px;">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { MonitoringRoutingModule } from "./monitoring-routing.module";
|
|||
import { MonitoringComponent } from "./monitoring.component";
|
||||
import { ClipboardModule } from "@angular/cdk/clipboard";
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -49,7 +50,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
|||
TableModule,
|
||||
TooltipModule,
|
||||
UtilitiesModule,
|
||||
InfiniteScrollModule
|
||||
InfiniteScrollModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [MonitoringComponent],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,30 +13,66 @@
|
|||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<gui-grid [rowHeight]="82" [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu"
|
||||
[sorting]="sorting" [autoResizeWidth]=true [paging]="paging">
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column width="auto" header="Perms" field="perms">
|
||||
<ng-template let-value="item.role" let-item="item" let-index="index">
|
||||
<div style="text-wrap: initial;">
|
||||
<ng-container *ngFor="let perm of item['perms'] | keyvalue">
|
||||
<c-badge *ngIf="perm.value" class="m-1" color="success">{{perm.key}}</c-badge>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="warning" size="sm" class="mx-1" (click)="editAddTask(item,'edit');"><i
|
||||
class="fa-regular fa-pen-to-square"></i></button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item);"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search permissions..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
|
||||
[showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name']"
|
||||
[loading]="loading">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Permission Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pResizableColumn>Perms</th>
|
||||
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>{{item.name}}</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<ng-container *ngFor="let perm of item['perms'] | keyvalue">
|
||||
<c-badge *ngIf="perm.value" color="success">{{perm.key}}</c-badge>
|
||||
</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
<button cButton color="warning" size="sm" (click)="editAddTask(item,'edit');" pTooltip="Edit Permission">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item);" pTooltip="Delete Permission">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="3" class="text-center p-4">No permissions found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
|
||||
import { Component, OnInit, QueryList, ViewChildren, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
|
|
@ -30,8 +25,10 @@ interface IUser {
|
|||
templateUrl: "permissions.component.html",
|
||||
})
|
||||
export class PermissionsComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
|
|
@ -63,7 +60,6 @@ export class PermissionsComponent implements OnInit {
|
|||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public SelectedPerm: any = {};
|
||||
|
|
@ -107,24 +103,9 @@ export class PermissionsComponent implements OnInit {
|
|||
closeButton: true,
|
||||
};
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
show_toast(title: string, body: string, color: string) {
|
||||
const { ...props } = { ...this.toasterForm, color, title, body };
|
||||
const componentRef = this.viewChildren.first.addToast(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ import { IconModule } from "@coreui/icons-angular";
|
|||
|
||||
import { PermissionsRoutingModule } from "./permissions-routing.module";
|
||||
import { PermissionsComponent } from "./permissions.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -33,7 +35,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
TooltipModule,
|
||||
ModalModule,
|
||||
FormsModule,
|
||||
BadgeModule,
|
||||
|
|
|
|||
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 {
|
||||
}
|
||||
712
src/app/views/sequences/sequences.component.html
Normal file
712
src/app/views/sequences/sequences.component.html
Normal file
|
|
@ -0,0 +1,712 @@
|
|||
<div style="position: relative;">
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4">
|
||||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="3">
|
||||
Sequences
|
||||
</c-col>
|
||||
<c-col xs [lg]="9">
|
||||
<h6 style="text-align: right;">
|
||||
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Sequence('','add')"
|
||||
style="color: #fff;"><i class="fa-solid fa-plus"></i> Create Sequence</button>
|
||||
<button cButton color="warning" class="mx-1" size="sm"
|
||||
(click)="ManageAlertsModalVisible = true" style="color: #000;"><i
|
||||
class="fa-solid fa-bell"></i> Manage Alerts</button>
|
||||
</h6>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search sequences..."
|
||||
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
|
||||
[showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name', 'created']" [loading]="false">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="source_snippet_id" pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Source Snippet</span>
|
||||
<span>
|
||||
<p-sortIcon field="source_snippet_id"></p-sortIcon>
|
||||
<p-columnFilter type="numeric" field="source_snippet_id" display="menu"
|
||||
class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="is_active" class="text-center" pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Active</span>
|
||||
<span>
|
||||
<p-sortIcon field="is_active"></p-sortIcon>
|
||||
<p-columnFilter type="boolean" field="is_active" display="menu"
|
||||
class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="created" pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Created</span>
|
||||
<span>
|
||||
<p-sortIcon field="created"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><strong>{{item.name}}</strong></td>
|
||||
<td>Snippet #{{item.source_snippet_id}}</td>
|
||||
<td class="text-center">
|
||||
<i *ngIf="item.is_active" class="fa-solid fa-check" style="color: green;"></i>
|
||||
<i *ngIf="!item.is_active" class="fa-solid fa-x" style="color: red;"></i>
|
||||
</td>
|
||||
<td>{{item.created}}</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
<button cButton color="success" size="sm" (click)="Run_Sequence(item)"
|
||||
pTooltip="Execute Sequence">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
<button cButton color="primary" size="sm" (click)="Edit_Sequence(item,'edit')"
|
||||
pTooltip="Edit Sequence">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button cButton color="info" size="sm" (click)="show_history(item)"
|
||||
pTooltip="Execution History">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)"
|
||||
pTooltip="Delete Sequence">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center p-4">No sequences found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
|
||||
<c-modal #EditSequenceModal backdrop="static" size="xl" [(visible)]="EditSequenceModalVisible" id="EditSequenceModal"
|
||||
class="builder-modal">
|
||||
<c-modal-header class="bg-light">
|
||||
<h5 *ngIf="ModalAction=='edit'" cModalTitle>
|
||||
<i class="fa-solid fa-edit me-2"></i>Edit Sequence: {{current_sequence['name']}}
|
||||
</h5>
|
||||
<h5 *ngIf="ModalAction=='add'" cModalTitle>
|
||||
<i class="fa-solid fa-plus me-2"></i>Create New Sequence
|
||||
</h5>
|
||||
<button [cModalToggle]="EditSequenceModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-4 bg-light">
|
||||
<!-- Basic Information -->
|
||||
<c-card class="mb-4 shadow-sm border-0">
|
||||
<c-card-header class="bg-white border-bottom-0 pt-3 pb-0">
|
||||
<h6 class="mb-1"><i class="fa-solid fa-info-circle text-primary me-2"></i>Sequence Details</h6>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<c-row class="g-3 align-items-center">
|
||||
<c-col xs="12" md="6">
|
||||
<label class="form-label text-muted small mb-1">Sequence Name</label>
|
||||
<input cFormControl placeholder="e.g. Daily Health Check" [(ngModel)]="current_sequence['name']"
|
||||
class="border-secondary" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="6">
|
||||
<label class="form-label text-muted small mb-1">Source Snippet (Entry Point)</label>
|
||||
<ngx-super-select [dataSource]="Snippets" [options]="snippetOptions"
|
||||
(selectionChanged)="onSelectSourceSnippet($event)"
|
||||
[selectedItemValues]="[current_sequence['source_snippet_id']]"
|
||||
class="styled border-secondary"></ngx-super-select>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<div class="mt-3 pt-3 border-top d-flex gap-4">
|
||||
<c-form-check>
|
||||
<input cFormCheckInput type="checkbox" id="isActiveSeq"
|
||||
[(ngModel)]="current_sequence['is_active']" />
|
||||
<label cFormCheckLabel for="isActiveSeq" class="user-select-none">Sequence Active</label>
|
||||
</c-form-check>
|
||||
<c-form-check>
|
||||
<input cFormCheckInput type="checkbox" id="storeHistorySeq"
|
||||
[(ngModel)]="current_sequence['store_all_history']" />
|
||||
<label cFormCheckLabel for="storeHistorySeq" class="user-select-none">Store Execution
|
||||
History</label>
|
||||
</c-form-check>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
||||
<!-- Flowchart Builder -->
|
||||
<c-card class="mb-3 shadow-sm border-0 flowchart-container">
|
||||
<c-card-header class="bg-white d-flex justify-content-between align-items-center py-3">
|
||||
<h6 class="mb-0"><i class="fa-solid fa-project-diagram text-primary me-2"></i>Condition Flowchart</h6>
|
||||
<button cButton color="primary" variant="outline" size="sm"
|
||||
(click)="addCondition(current_sequence.conditions_json)">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Root Condition
|
||||
</button>
|
||||
</c-card-header>
|
||||
<c-card-body class="bg-light p-4 overflow-auto bg-grid-pattern">
|
||||
|
||||
<div *ngIf="!current_sequence.conditions_json || current_sequence.conditions_json.length === 0"
|
||||
class="text-center py-5 text-muted">
|
||||
<i class="fa-solid fa-code-branch fa-3x mb-3 opacity-25"></i>
|
||||
<h6>No conditions defined</h6>
|
||||
<p class="small mb-0">The source snippet will execute unconditionally.<br />Add a condition below to
|
||||
build branching logic based on the snippet's output.</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-tree"
|
||||
*ngIf="current_sequence.conditions_json && current_sequence.conditions_json.length > 0">
|
||||
<!-- Root Entry Node -->
|
||||
<div class="flow-entry-node">
|
||||
<div class="entry-bubble"><i class="fa-solid fa-play me-2"></i>Source Snippet Output</div>
|
||||
<div class="flow-connector-down"></div>
|
||||
</div>
|
||||
|
||||
<!-- Recursive Conditions Render -->
|
||||
<ng-container
|
||||
*ngTemplateOutlet="conditionNodeTemplate; context: { conditions: current_sequence.conditions_json }"></ng-container>
|
||||
</div>
|
||||
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-modal-body>
|
||||
<c-modal-footer class="bg-white border-top-0 pt-0 pb-3 pe-4">
|
||||
<button cButton color="secondary" variant="ghost" [cModalToggle]="EditSequenceModal.id">Cancel</button>
|
||||
<button cButton color="primary" class="px-4" (click)="save_sequence()"><i class="fa-solid fa-save me-2"></i>Save
|
||||
Sequence</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- RECURSIVE TEMPLATE FOR CONDITIONS -->
|
||||
<ng-template #conditionNodeTemplate let-conditions="conditions">
|
||||
<div class="condition-group" *ngIf="conditions && conditions.length > 0">
|
||||
<div class="condition-branch" *ngFor="let cond of conditions; let i = index">
|
||||
|
||||
<!-- Condition Card -->
|
||||
<div class="node-card condition-node">
|
||||
<div
|
||||
class="node-header bg-warning bg-opacity-10 text-warning d-flex justify-content-between align-items-center">
|
||||
<span><i class="fa-solid fa-code-compare me-2"></i>Condition</span>
|
||||
<button class="btn btn-link text-danger p-0 m-0" (click)="removeNode(conditions, i)"><i
|
||||
class="fa-solid fa-times"></i></button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<c-row class="g-2 align-items-center">
|
||||
<c-col xs="12" md="12" class="mb-2">
|
||||
<select class="form-select form-select-sm" [(ngModel)]="cond.type">
|
||||
<option value="contains">Contains Content</option>
|
||||
<option value="not_contain">Does Not Contain</option>
|
||||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="12">
|
||||
<div class="input-group input-group-sm">
|
||||
<div class="input-group-text bg-white border-end-0 pe-1">
|
||||
<input class="form-check-input mt-0" type="checkbox" [(ngModel)]="cond.is_regex"
|
||||
title="Use Regex">
|
||||
<small class="ms-1 text-muted" title="Use Regex">Regex</small>
|
||||
</div>
|
||||
<input type="text" class="form-control border-start-0 ps-1" [(ngModel)]="cond.pattern"
|
||||
placeholder="Match string or pattern...">
|
||||
</div>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<div class="node-footer mt-2 pt-2 border-top text-center">
|
||||
<button class="btn btn-sm btn-outline-primary py-0" style="font-size: 0.75rem"
|
||||
(click)="addAction(cond)">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Action
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flow Connector from Condition to Actions -->
|
||||
<div class="flow-connector-down" *ngIf="cond.actions && cond.actions.length > 0"></div>
|
||||
|
||||
<!-- Render Actions under this condition -->
|
||||
<div class="action-group" *ngIf="cond.actions && cond.actions.length > 0">
|
||||
<div class="action-branch" *ngFor="let act of cond.actions; let j = index">
|
||||
|
||||
<div class="node-card action-node">
|
||||
<div
|
||||
class="node-header bg-success bg-opacity-10 text-success d-flex justify-content-between align-items-center">
|
||||
<span><i class="fa-solid fa-bolt me-2"></i>Action</span>
|
||||
<button class="btn btn-link text-danger p-0 m-0" (click)="removeNode(cond.actions, j)"><i
|
||||
class="fa-solid fa-times"></i></button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<select class="form-select form-select-sm mb-2" [(ngModel)]="act.action_type">
|
||||
<option value="alert_set">Trigger Alert</option>
|
||||
<option value="alert_clear">Clear Alert</option>
|
||||
<option value="execute_snippet">Execute Another Snippet</option>
|
||||
</select>
|
||||
|
||||
<!-- Alert Selection -->
|
||||
<div *ngIf="act.action_type === 'alert_set' || act.action_type === 'alert_clear'">
|
||||
<select class="form-select form-select-sm" [(ngModel)]="act.alert_id">
|
||||
<option [ngValue]="null">Select an Alert Definition...</option>
|
||||
<option *ngFor="let a of Alerts" [value]="a.id">{{a.name}} ({{a.level}})</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Snippet Selection -->
|
||||
<div *ngIf="act.action_type === 'execute_snippet'">
|
||||
<select class="form-select form-select-sm" [(ngModel)]="act.snippet_id">
|
||||
<option [ngValue]="null">Select a Snippet to Run...</option>
|
||||
<option *ngFor="let s of Snippets" [value]="s.id">{{s.name}}</option>
|
||||
</select>
|
||||
|
||||
<!-- Recursive Conditions for nested Snippet -->
|
||||
<div class="mt-2 pt-2 border-top text-center">
|
||||
<button class="btn btn-sm btn-outline-warning py-0" style="font-size: 0.75rem"
|
||||
(click)="addCondition(act.conditions || (act.conditions = []))">
|
||||
<i class="fa-solid fa-code-compare me-1"></i>Add Nested Condition
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recursive render of nested conditions from Snippet Action -->
|
||||
<div *ngIf="act.action_type === 'execute_snippet' && act.conditions && act.conditions.length > 0">
|
||||
<div class="flow-connector-down"></div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="conditionNodeTemplate; context: { conditions: act.conditions }"></ng-container>
|
||||
</div>
|
||||
|
||||
</div> <!-- end action-branch loop -->
|
||||
</div> <!-- end action-group -->
|
||||
|
||||
</div> <!-- end condition-branch loop -->
|
||||
</div> <!-- end condition-group -->
|
||||
</ng-template>
|
||||
|
||||
<!-- Manage Alerts Modal -->
|
||||
<c-modal #ManageAlertsModal backdrop="static" size="lg" [(visible)]="ManageAlertsModalVisible" id="ManageAlertsModal">
|
||||
<c-modal-header class="bg-light">
|
||||
<h5 cModalTitle><i class="fa-solid fa-bell me-2"></i>Manage Alert Definitions</h5>
|
||||
<button [cModalToggle]="ManageAlertsModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-4 bg-light">
|
||||
<c-row>
|
||||
<!-- Form Column -->
|
||||
<c-col xs="12" md="4" class="mb-4">
|
||||
<c-card class="shadow-sm border-0">
|
||||
<c-card-header class="bg-white">
|
||||
<h6 class="mb-0">{{editingAlert.id === 0 ? 'Create Alert' : 'Edit Alert'}}</h6>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Alert Name</label>
|
||||
<input class="form-control" [(ngModel)]="editingAlert.name" placeholder="e.g. CPU High">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Severity Level</label>
|
||||
<select class="form-select" [(ngModel)]="editingAlert.level">
|
||||
<option value="Warning">Warning</option>
|
||||
<option value="Critical">Critical</option>
|
||||
<option value="Error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<button cButton color="primary" class="w-100" (click)="saveAlert()">
|
||||
<i class="fa-solid fa-save me-1"></i>Save Alert
|
||||
</button>
|
||||
<button *ngIf="editingAlert.id !== 0" cButton color="secondary" variant="ghost"
|
||||
class="w-100 mt-2" (click)="editingAlert = { id: 0, name: '', level: 'info' }">Cancel
|
||||
Edit</button>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
||||
<!-- List Column -->
|
||||
<c-col xs="12" md="8">
|
||||
<c-card class="shadow-sm border-0">
|
||||
<c-card-header class="bg-white">
|
||||
<h6 class="mb-0">Existing Alerts</h6>
|
||||
</c-card-header>
|
||||
<c-card-body class="p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Level</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let alert of Alerts">
|
||||
<td class="fw-semibold">{{alert.name}}</td>
|
||||
<td>
|
||||
<c-badge
|
||||
[color]="alert.level === 'critical' ? 'danger' : (alert.level === 'warning' ? 'warning' : 'info')">
|
||||
{{alert.level | uppercase}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button cButton color="info" size="sm" variant="ghost" class="me-1"
|
||||
(click)="editAlert(alert)"><i class="fa-solid fa-edit"></i></button>
|
||||
<button cButton color="danger" size="sm" variant="ghost"
|
||||
(click)="deleteAlert(alert.id)"><i
|
||||
class="fa-solid fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="Alerts.length === 0">
|
||||
<td colspan="3" class="text-center text-muted py-4">No alerts defined.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-modal-body>
|
||||
</c-modal>
|
||||
|
||||
<!-- Execution History Modal -->
|
||||
<c-modal #HistoryModal backdrop="static" size="xl" [(visible)]="HistoryModalVisible" id="HistoryModal">
|
||||
<c-modal-header class="bg-light">
|
||||
<h5 cModalTitle><i class="fa-solid fa-clock-rotate-left me-2"></i>Execution History: {{viewing_sequence_name}}
|
||||
</h5>
|
||||
<button [cModalToggle]="HistoryModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-4 bg-light">
|
||||
<c-card class="shadow-sm border-0">
|
||||
<c-card-body class="p-0">
|
||||
<div *ngIf="sequence_history.length > 0">
|
||||
<div *ngIf="sequence_history.length > 0">
|
||||
<div *ngFor="let run of sequence_history; let i = index" class="mb-3">
|
||||
<c-card class="border-0 shadow-sm overflow-hidden">
|
||||
<c-card-header (click)="run.visible = !run.visible" style="cursor: pointer;"
|
||||
class="d-flex justify-content-between align-items-center bg-white border-0 py-3">
|
||||
<span>
|
||||
<i class="fa-solid"
|
||||
[ngClass]="run.visible ? 'fa-chevron-down' : 'fa-chevron-right'"
|
||||
class="me-3 text-muted"></i>
|
||||
<i class="fa-solid fa-calendar-check me-2 text-primary"></i>
|
||||
<strong>{{run.created}}</strong>
|
||||
<small class="text-muted ms-3">Exec ID: {{run.exec_id | slice:0:8}}...</small>
|
||||
</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<c-badge
|
||||
[color]="run.successCount === run.totalCount ? 'success' : (run.successCount === 0 ? 'danger' : 'warning')"
|
||||
class="px-3 py-2 text-white">
|
||||
{{run.successCount}}/{{run.totalCount}} Success
|
||||
</c-badge>
|
||||
</div>
|
||||
</c-card-header>
|
||||
<div [visible]="run.visible" cCollapse class="border-top">
|
||||
<c-card-body class="p-0 border-top">
|
||||
<p-table [value]="run.devices" [paginator]="true" [rows]="5"
|
||||
[showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm"
|
||||
class="border-0">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="device_id">Device <p-sortIcon
|
||||
field="device_id"></p-sortIcon></th>
|
||||
<th pSortableColumn="status" class="text-center"
|
||||
style="width: 150px">Status <p-sortIcon
|
||||
field="status"></p-sortIcon></th>
|
||||
<th style="width: 150px" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-dev>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold ms-2">
|
||||
<i class="fa-solid fa-microchip me-2 opacity-50"></i>Device
|
||||
#{{dev.device_id}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<c-badge
|
||||
[color]="dev.status === 'success' ? 'success' : 'danger'"
|
||||
class="text-white px-2">
|
||||
{{dev.status | uppercase}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button cButton color="dark" size="sm" variant="outline"
|
||||
pTooltip="Trace Log" (click)="showTrace(dev)">
|
||||
<i class="fa-solid fa-terminal"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="sequence_history.length === 0" class="text-center text-muted py-5">
|
||||
<i class="fa-solid fa-inbox fa-3x mb-3 opacity-25"></i>
|
||||
<h6>No History Found</h6>
|
||||
<p class="small mb-0">This sequence hasn't been executed yet or history tracking is disabled.</p>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-modal-body>
|
||||
</c-modal>
|
||||
|
||||
<!-- Trace Log Modal (Terminal Style) -->
|
||||
<c-modal #TraceModal backdrop="static" size="xl" [(visible)]="TraceModalVisible" id="TraceModal">
|
||||
<c-modal-header class="bg-dark text-white">
|
||||
<h5 cModalTitle><i class="fa-solid fa-terminal me-2"></i>Execution Trace: Device #{{viewing_device_id}}</h5>
|
||||
<button (click)="TraceModalVisible=false" cButtonClose white></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-0 bg-dark">
|
||||
<div class="nav nav-tabs border-secondary px-3 pt-2">
|
||||
<div class="nav-item">
|
||||
<a [cTabContent]="tabContent" [active]="true" [tabPaneIdx]="0"
|
||||
class="nav-link text-light border-secondary cursor-pointer">
|
||||
<i class="fa-solid fa-code me-2"></i>SSH Output
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a [cTabContent]="tabContent" [active]="false" [tabPaneIdx]="1"
|
||||
class="nav-link text-light border-secondary cursor-pointer">
|
||||
<i class="fa-solid fa-list-check me-2"></i>Evaluation Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<c-tab-content #tabContent="cTabContent" class="p-3">
|
||||
<c-tab-pane class="p-0">
|
||||
<div class="terminal-container">
|
||||
<div class="terminal-content">
|
||||
<div highlight-js lang="routeros" [options]="{}">{{
|
||||
current_trace.parsedLog?.ssh_output || current_trace.task_log }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-tab-pane>
|
||||
<c-tab-pane class="p-3 text-light">
|
||||
<div class="evaluation-tree" *ngIf="current_trace.parsedLog?.evaluation">
|
||||
<!-- Structured JSON Evaluation -->
|
||||
<div *ngIf="isObject(current_trace.parsedLog.evaluation); else flatEval">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="evalNodeTemplate; context: { node: current_trace.parsedLog.evaluation }"></ng-container>
|
||||
</div>
|
||||
<!-- Legacy String Evaluation -->
|
||||
<ng-template #flatEval>
|
||||
<pre class="text-info bg-transparent border-0 p-0 m-0"
|
||||
style="white-space: pre-wrap;">{{current_trace.parsedLog.evaluation}}</pre>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="!current_trace.parsedLog?.evaluation" class="text-center py-4 text-muted">
|
||||
No evaluation details found for this run.
|
||||
</div>
|
||||
</c-tab-pane>
|
||||
</c-tab-content>
|
||||
</c-modal-body>
|
||||
<c-modal-footer class="bg-dark border-secondary">
|
||||
<button cButton color="secondary" (click)="TraceModalVisible=false">Close Trace</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- RECURSIVE TEMPLATE FOR EVALUATION LOGS -->
|
||||
<ng-template #evalNodeTemplate let-node="node">
|
||||
<div class="eval-node ms-3 border-start border-secondary ps-3 mb-3">
|
||||
<div *ngFor="let rule of (isArray(node) ? node : [node])" class="mb-2">
|
||||
<div class="d-flex align-items-center mb-1">
|
||||
<i class="fa-solid fa-check-circle text-success me-2" *ngIf="rule.matched"></i>
|
||||
<i class="fa-regular fa-circle text-muted me-2" *ngIf="!rule.matched"></i>
|
||||
<span [class.text-white]="rule.matched" [class.text-muted]="!rule.matched">
|
||||
Rule <strong>[{{rule.type}}]</strong>: '{{rule.pattern}}' →
|
||||
<span [class.text-success]="rule.matched">{{rule.matched ? 'MATCHED' : 'NO MATCH'}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="rule.matched && rule.actions?.length > 0" class="ms-4 my-2 p-2 bg-opacity-10 bg-info rounded">
|
||||
<div *ngFor="let act of rule.actions" class="mb-1 small">
|
||||
<i class="fa-solid fa-arrow-right text-info me-2"></i>
|
||||
<strong>ACTION:</strong> {{act.action_type}}
|
||||
<span *ngIf="act.alert_id" class="text-warning ms-1">(Alert ID: {{act.alert_id}})</span>
|
||||
<span *ngIf="act.snippet_id" class="text-primary ms-1">(Snippet: {{act.snippet_id}})</span>
|
||||
|
||||
<!-- Nested Evaluation if another snippet was run -->
|
||||
<div *ngIf="act.nested_eval" class="mt-2">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="evalNodeTemplate; context: { node: act.nested_eval }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- MANUAL EXECUTION MODAL -->
|
||||
<c-modal #ExecSequenceModal backdrop="static" size="xl" [(visible)]="ExecSequenceModalVisible" id="ExecSequenceModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Exec Sequence</h5>
|
||||
<button [cModalToggle]="ExecSequenceModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input cFormControl id="floatingExecName" placeholder="current_sequence['name']"
|
||||
[(ngModel)]="current_sequence['name']" disabled="true" />
|
||||
<label cLabel for="floatingExecName">Sequence Name</label>
|
||||
</div>
|
||||
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input cFormControl id="floatingExecDesc" placeholder="Description"
|
||||
[(ngModel)]="current_sequence['description']" />
|
||||
<label cLabel for="floatingExecDesc">Description</label>
|
||||
</div>
|
||||
|
||||
<c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelectSeq">
|
||||
Member type
|
||||
</label>
|
||||
<select cSelect id="inputGroupSelectSeq" (change)="form_changed()"
|
||||
[(ngModel)]="current_sequence['selection_type']">
|
||||
<option value="devices">Devices</option>
|
||||
<option value="groups">Groups</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
|
||||
<h5>Members :</h5>
|
||||
<p-table #dtMembers [value]="SelectedMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name">Name <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th *ngIf="current_sequence['selection_type']=='devices'" pSortableColumn="mac">MAC <p-sortIcon
|
||||
field="mac"></p-sortIcon></th>
|
||||
<th style="width: 100px" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>{{item.name}}</td>
|
||||
<td *ngIf="current_sequence['selection_type']=='devices'">{{item.mac}}</td>
|
||||
<td class="text-center">
|
||||
<button cButton color="danger" size="sm" (click)="remove_member(item)" pTooltip="Remove Member">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td [attr.colspan]="current_sequence['selection_type']=='devices' ? 3 : 2" class="text-center p-2">
|
||||
No members added.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<hr />
|
||||
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
|
||||
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="submit_exec()" cButton color="primary">Execute</button>
|
||||
<button [cModalToggle]="ExecSequenceModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- MEMBER SELECTION MODAL -->
|
||||
<c-modal #NewMemberModal backdrop="static" size="lg" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Editing Group </h5>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<c-input-group class="mb-3">
|
||||
<h5>Group Members :</h5>
|
||||
<div class="mb-3 d-flex justify-content-end w-100">
|
||||
<span class="p-input-icon-left table-search-input">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search members..."
|
||||
(input)="applyFilterNewMembers($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm" [(selection)]="SelectedNewMemberRows"
|
||||
(selectionChange)="onSelectedRowsNewMembers($event)" [globalFilterFields]="['name', 'ip', 'mac']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Member Name</span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th *ngIf="current_sequence['selection_type']=='devices'" pSortableColumn="ip" pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>IP Address</span>
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th *ngIf="current_sequence['selection_type']=='devices'" pSortableColumn="mac"
|
||||
pResizableColumn>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>MAC Address</span>
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
|
||||
<td>{{item.name}}</td>
|
||||
<td *ngIf="current_sequence['selection_type']=='devices'">{{item.ip}}</td>
|
||||
<td *ngIf="current_sequence['selection_type']=='devices'">{{item.mac}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td [attr.colspan]="current_sequence['selection_type']=='devices' ? 4 : 2"
|
||||
class="text-center p-4">No available members found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<br />
|
||||
</c-input-group>
|
||||
<hr />
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="add_new_members()" cButton color="primary">Add Selected</button>
|
||||
<button (click)="NewMemberModalVisible=false" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
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;
|
||||
}
|
||||
413
src/app/views/sequences/sequences.component.ts
Normal file
413
src/app/views/sequences/sequences.component.ts
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { ToastComponent } from '@coreui/angular';
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sequences',
|
||||
templateUrl: './sequences.component.html',
|
||||
styleUrls: ['./sequences.component.scss']
|
||||
})
|
||||
export class SequencesComponent implements OnInit {
|
||||
|
||||
public sequences: any[] = [];
|
||||
public source: any[] = [];
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
@ViewChild('dtMembers') tableMembers!: Table;
|
||||
@ViewChild('dtNewMembers') tableNewMembers!: Table;
|
||||
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
applyFilterMembers($event: any, stringVal: string) {
|
||||
this.tableMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
applyFilterNewMembers($event: any, stringVal: string) {
|
||||
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public EditSequenceModalVisible: boolean = false;
|
||||
public current_sequence: any = {
|
||||
id: 0,
|
||||
name: '',
|
||||
source_snippet_id: null,
|
||||
store_all_history: false,
|
||||
is_active: true,
|
||||
conditions_json: []
|
||||
};
|
||||
|
||||
public ManageAlertsModalVisible: boolean = false;
|
||||
public editingAlert: any = { id: 0, name: '', level: 'Info' };
|
||||
|
||||
public Snippets: any = [];
|
||||
public Alerts: any = [];
|
||||
public ModalAction: string = "add";
|
||||
public ExecSequenceModalVisible: boolean = false;
|
||||
public SelectedSequence: any = {};
|
||||
public SelectedMembers: any[] = [];
|
||||
public SelectedTaskItems: any[] = [];
|
||||
public availbleMembers: any[] = [];
|
||||
public NewMemberModalVisible: boolean = false;
|
||||
public SelectedNewMemberRows: any[] = [];
|
||||
public NewMemberRows: any[] = [];
|
||||
|
||||
public loading: boolean = true;
|
||||
|
||||
// super select options
|
||||
snippetOptions: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Select Snippet",
|
||||
searchEnabled: true,
|
||||
};
|
||||
|
||||
alertOptions: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Select Alert",
|
||||
searchEnabled: true,
|
||||
};
|
||||
|
||||
constructor(public MikroWizardRPC: dataProvider) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadSequences();
|
||||
this.loadSnippets();
|
||||
this.loadAlerts();
|
||||
}
|
||||
|
||||
loadSequences() {
|
||||
this.MikroWizardRPC.get_sequences().then(
|
||||
(res: any) => {
|
||||
if (res) {
|
||||
this.source = res.map((seq: any) => {
|
||||
if (typeof seq.conditions_json === 'string') {
|
||||
try {
|
||||
seq.conditions_json = JSON.parse(seq.conditions_json);
|
||||
} catch (e) {
|
||||
seq.conditions_json = [];
|
||||
}
|
||||
}
|
||||
if (!seq.conditions_json) seq.conditions_json = [];
|
||||
return seq;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
// Handle error visually if needed
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadSnippets(): void {
|
||||
this.MikroWizardRPC.get_snippets("", "", "", 0, 1000, false).then((res: any) => {
|
||||
this.Snippets = res.map((x: any) => ({ id: x.id, name: x.name }));
|
||||
});
|
||||
}
|
||||
|
||||
loadAlerts(): void {
|
||||
this.MikroWizardRPC.get_alerts().then((res: any) => {
|
||||
this.Alerts = res.map((x: any) => ({ id: x.id, name: x.name, level: x.level }));
|
||||
});
|
||||
}
|
||||
|
||||
addCondition(targetArray: any[]) {
|
||||
targetArray.push({
|
||||
type: 'contains',
|
||||
is_regex: false,
|
||||
pattern: '',
|
||||
actions: []
|
||||
});
|
||||
}
|
||||
|
||||
addAction(condition: any) {
|
||||
if (!condition.actions) condition.actions = [];
|
||||
condition.actions.push({
|
||||
action_type: 'alert_set',
|
||||
alert_id: null,
|
||||
snippet_id: null,
|
||||
conditions: []
|
||||
});
|
||||
}
|
||||
|
||||
removeNode(array: any[], index: number) {
|
||||
if (array && array.length > index) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Edit_Sequence(item: any, mode: string) {
|
||||
if (mode === 'add') {
|
||||
this.current_sequence = {
|
||||
id: 0,
|
||||
name: '',
|
||||
source_snippet_id: null,
|
||||
store_all_history: false,
|
||||
is_active: true,
|
||||
conditions_json: []
|
||||
};
|
||||
this.ModalAction = 'add';
|
||||
} else {
|
||||
// deep copy
|
||||
this.current_sequence = JSON.parse(JSON.stringify(item));
|
||||
if (!this.current_sequence.conditions_json) this.current_sequence.conditions_json = [];
|
||||
this.ModalAction = 'edit';
|
||||
}
|
||||
this.EditSequenceModalVisible = true;
|
||||
}
|
||||
|
||||
save_sequence() {
|
||||
let payload = { ...this.current_sequence };
|
||||
if (payload.conditions_json && typeof payload.conditions_json !== 'string') {
|
||||
payload.conditions_json = JSON.stringify(payload.conditions_json);
|
||||
}
|
||||
this.MikroWizardRPC.save_sequence(payload).then(() => {
|
||||
this.EditSequenceModalVisible = false;
|
||||
this.loadSequences();
|
||||
});
|
||||
}
|
||||
|
||||
onSelectSourceSnippet($event: any) {
|
||||
this.current_sequence.source_snippet_id = $event;
|
||||
}
|
||||
|
||||
// --- Alert Management ---
|
||||
openManageAlerts() {
|
||||
this.ManageAlertsModalVisible = true;
|
||||
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
|
||||
}
|
||||
|
||||
editAlert(item: any) {
|
||||
this.editingAlert = { ...item };
|
||||
}
|
||||
|
||||
saveAlert() {
|
||||
this.MikroWizardRPC.save_alert(this.editingAlert).then(() => {
|
||||
this.loadAlerts();
|
||||
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
|
||||
});
|
||||
}
|
||||
|
||||
deleteAlert(id: number) {
|
||||
if (confirm("Are you sure you want to delete this alert?")) {
|
||||
this.MikroWizardRPC.delete_alert(id).then(() => {
|
||||
this.loadAlerts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
confirm_delete(item: any, confirm: boolean) {
|
||||
if (!confirm) {
|
||||
if (window.confirm("Are you sure you want to delete sequence: " + item.name + "?")) {
|
||||
this.MikroWizardRPC.delete_sequence(item.id).then(() => {
|
||||
this.loadSequences();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
public HistoryModalVisible: boolean = false;
|
||||
public sequence_history: any[] = [];
|
||||
public viewing_sequence_name: string = '';
|
||||
|
||||
public TraceModalVisible: boolean = false;
|
||||
public current_trace: any = {};
|
||||
public viewing_device_id: number = 0;
|
||||
|
||||
showTrace(dev: any) {
|
||||
this.current_trace = dev;
|
||||
this.viewing_device_id = dev.device_id;
|
||||
this.TraceModalVisible = true;
|
||||
}
|
||||
|
||||
show_history(item: any) {
|
||||
this.viewing_sequence_name = item.name;
|
||||
this.sequence_history = [];
|
||||
this.HistoryModalVisible = true;
|
||||
this.MikroWizardRPC.get_sequence_history(item.id).then((res: any) => {
|
||||
// Process the nested history response
|
||||
this.sequence_history = res.map((run: any) => {
|
||||
const total = run.devices.length;
|
||||
const success = run.devices.filter((d: any) => d.status === 'success').length;
|
||||
return {
|
||||
...run,
|
||||
successCount: success,
|
||||
totalCount: total,
|
||||
visible: false,
|
||||
devices: run.devices.map((dev: any) => ({
|
||||
...dev,
|
||||
parsedLog: this.parseLog(dev.task_log)
|
||||
}))
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseLog(logJson: any) {
|
||||
if (!logJson) return { ssh_output: '', evaluation: '' };
|
||||
|
||||
let processed = logJson;
|
||||
// Normalize any escaped newlines if it's a string
|
||||
if (typeof logJson === 'string') {
|
||||
processed = logJson.split('\\n').join('\n').replace(/\r\n/g, '\n');
|
||||
}
|
||||
|
||||
// If it's already an object, use it directly (but normalize ssh_output)
|
||||
if (typeof processed === 'object' && processed !== null) {
|
||||
if (processed.ssh_output) {
|
||||
processed.ssh_output = processed.ssh_output.split('\\n').join('\n').replace(/\r\n/g, '\n');
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
// Try to parse as JSON first (new format)
|
||||
try {
|
||||
if (typeof processed === 'string' && processed.trim().startsWith('{')) {
|
||||
const parsed = JSON.parse(processed);
|
||||
if (parsed.ssh_output) {
|
||||
parsed.ssh_output = parsed.ssh_output.split('\\n').join('\n').replace(/\r\n/g, '\n');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue to legacy parsing
|
||||
}
|
||||
|
||||
// Legacy Interleaved Log parsing
|
||||
if (typeof processed !== 'string') return { ssh_output: processed, evaluation: '' };
|
||||
|
||||
const lines = processed.split('\n');
|
||||
let outputSections: string[] = [];
|
||||
let evalSections: string[] = [];
|
||||
|
||||
let currentSection: string[] = [];
|
||||
let isCurrentOutput = true;
|
||||
|
||||
for (let line of lines) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
const startsNewOutput = lowerLine.includes('output (') || (lowerLine.includes('ssh output') && !lowerLine.includes('failed'));
|
||||
const startsNewEval = lowerLine.includes('evaluation');
|
||||
|
||||
if (startsNewOutput || startsNewEval) {
|
||||
// Save previous section
|
||||
if (currentSection.length > 0) {
|
||||
const block = currentSection.join('\n').trim();
|
||||
if (isCurrentOutput) outputSections.push(block);
|
||||
else evalSections.push(block);
|
||||
}
|
||||
// Start new section
|
||||
currentSection = [line];
|
||||
isCurrentOutput = startsNewOutput;
|
||||
} else {
|
||||
currentSection.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Final section
|
||||
if (currentSection.length > 0) {
|
||||
const block = currentSection.join('\n').trim();
|
||||
if (isCurrentOutput) outputSections.push(block);
|
||||
else evalSections.push(block);
|
||||
}
|
||||
|
||||
return {
|
||||
ssh_output: outputSections.join('\n\n').trim(),
|
||||
evaluation: evalSections.join('\n\n').trim()
|
||||
};
|
||||
}
|
||||
|
||||
// --- Manual Execution ---
|
||||
Run_Sequence(item: any) {
|
||||
this.SelectedSequence = item;
|
||||
this.current_sequence = { ...item };
|
||||
this.current_sequence["selection_type"] = "devices";
|
||||
this.SelectedMembers = [];
|
||||
this.SelectedTaskItems = [];
|
||||
this.ExecSequenceModalVisible = true;
|
||||
}
|
||||
|
||||
form_changed() {
|
||||
this.SelectedMembers = [];
|
||||
this.SelectedTaskItems = [];
|
||||
}
|
||||
|
||||
remove_member(item: any) {
|
||||
this.SelectedMembers = this.SelectedMembers.filter(
|
||||
(x: any) => x.id != item.id
|
||||
);
|
||||
this.SelectedTaskItems = this.SelectedMembers.map((x: any) => {
|
||||
return x.id;
|
||||
});
|
||||
}
|
||||
|
||||
show_new_member_form() {
|
||||
this.NewMemberModalVisible = true;
|
||||
this.availbleMembers = [];
|
||||
this.SelectedNewMemberRows = [];
|
||||
this.NewMemberRows = [];
|
||||
|
||||
var data = {
|
||||
group_id: false,
|
||||
search: false,
|
||||
page: false,
|
||||
size: 10000,
|
||||
};
|
||||
|
||||
if (this.current_sequence["selection_type"] == "devices")
|
||||
this.MikroWizardRPC.get_dev_list(data).then((res: any) => {
|
||||
this.availbleMembers = res.filter(
|
||||
(x: any) => !this.SelectedTaskItems.includes(x.id)
|
||||
);
|
||||
});
|
||||
else
|
||||
this.MikroWizardRPC.get_devgroup_list().then((res: any) => {
|
||||
this.availbleMembers = res.filter(
|
||||
(x: any) => !this.SelectedTaskItems.includes(x.id)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onSelectedRowsNewMembers(rows: any): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: any) => m.source);
|
||||
}
|
||||
|
||||
isObject(val: any): boolean {
|
||||
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
||||
}
|
||||
|
||||
isArray(val: any): boolean {
|
||||
return Array.isArray(val);
|
||||
}
|
||||
|
||||
add_new_members() {
|
||||
this.SelectedMembers = [
|
||||
...new Set(this.SelectedMembers.concat(this.SelectedNewMemberRows)),
|
||||
];
|
||||
|
||||
this.SelectedTaskItems = this.SelectedMembers.map((x: any) => {
|
||||
return x.id;
|
||||
});
|
||||
|
||||
this.NewMemberModalVisible = false;
|
||||
}
|
||||
|
||||
submit_exec() {
|
||||
const payload = {
|
||||
sequence_id: this.SelectedSequence.id,
|
||||
selection_type: this.current_sequence.selection_type,
|
||||
members: this.SelectedTaskItems
|
||||
};
|
||||
this.MikroWizardRPC.exec_sequence(payload).then((res: any) => {
|
||||
this.ExecSequenceModalVisible = false;
|
||||
// Optionally show success toast or refresh history
|
||||
});
|
||||
}
|
||||
}
|
||||
56
src/app/views/sequences/sequences.module.ts
Normal file
56
src/app/views/sequences/sequences.module.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
ButtonGroupModule,
|
||||
ButtonModule,
|
||||
CardModule,
|
||||
FormModule,
|
||||
GridModule,
|
||||
ToastModule,
|
||||
ModalModule,
|
||||
BadgeModule,
|
||||
AccordionModule,
|
||||
NavModule,
|
||||
TabsModule,
|
||||
AlertModule,
|
||||
CollapseModule
|
||||
} from "@coreui/angular";
|
||||
import { HighlightJsModule } from 'ngx-highlight-js';
|
||||
import { SequencesRoutingModule } from "./sequences-routing.module";
|
||||
import { SequencesComponent } from "./sequences.component";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { NgxSuperSelectModule } from "ngx-super-select";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SequencesRoutingModule,
|
||||
CardModule,
|
||||
CommonModule,
|
||||
GridModule,
|
||||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
TooltipModule,
|
||||
ModalModule,
|
||||
ToastModule,
|
||||
FormsModule,
|
||||
BadgeModule,
|
||||
NgxSuperSelectModule,
|
||||
AccordionModule,
|
||||
NavModule,
|
||||
TabsModule,
|
||||
AlertModule,
|
||||
HighlightJsModule,
|
||||
CollapseModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [SequencesComponent],
|
||||
})
|
||||
export class SequencesModule { }
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,12 +7,11 @@
|
|||
}
|
||||
|
||||
.mdc-line-ripple.mdc-line-ripple--deactivating.ng-star-inserted {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Settings Container */
|
||||
.settings-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
}
|
||||
|
||||
.settings-header {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
// background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
|
@ -92,12 +91,12 @@
|
|||
padding: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
.section-card:hover {
|
||||
border-color: #0a58ca;
|
||||
background: linear-gradient(135deg, #cfe2ff 0%, #e7f3ff 100%);
|
||||
}
|
||||
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -205,7 +204,8 @@
|
|||
|
||||
|
||||
/* Form Controls */
|
||||
.form-control, .form-select {
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ced4da;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
|
@ -213,7 +213,8 @@
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
|
@ -250,29 +251,30 @@
|
|||
|
||||
/* Mobile Responsive for Informative Elements */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.info-banner,
|
||||
.warning-banner,
|
||||
.success-banner {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
.help-icon {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.setting-help {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.switch-description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
.switch-description small {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
|
||||
::ng-deep .tooltip-inner {
|
||||
max-width: 250px;
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -280,6 +282,7 @@
|
|||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
|
||||
.info-banner,
|
||||
.warning-banner,
|
||||
.success-banner {
|
||||
|
|
@ -287,7 +290,7 @@
|
|||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
.setting-label {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
|
@ -300,40 +303,40 @@
|
|||
.settings-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.settings-tabs {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.tab-link {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
.status-section,
|
||||
.config-section,
|
||||
.firmware-settings {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.download-section .section-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.switch-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.switch-info {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
|
@ -343,15 +346,15 @@
|
|||
.settings-container {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.settings-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
.tab-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.status-section,
|
||||
.config-section,
|
||||
.firmware-settings {
|
||||
|
|
@ -421,8 +424,15 @@
|
|||
|
||||
/* Animation for dropdown */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.search-dropdown {
|
||||
|
|
@ -618,16 +628,4 @@
|
|||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Grid Enhancements */
|
||||
::ng-deep .gui-grid {
|
||||
.gui-grid-header {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gui-grid-cell {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,21 +4,12 @@ import {
|
|||
QueryList,
|
||||
ViewChildren,
|
||||
ViewEncapsulation,
|
||||
ViewChild
|
||||
} from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSelectedRow,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
import { TimeZones } from "./timezones-data";
|
||||
|
|
@ -30,14 +21,16 @@ import { TimeZones } from "./timezones-data";
|
|||
})
|
||||
|
||||
export class SettingsComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public ispro:boolean=false;
|
||||
public filterText: string;
|
||||
public filterText: string = '';
|
||||
public filters: any = {};
|
||||
public firms: any = {};
|
||||
public firmtodownload: any = {};
|
||||
public activeTab: string = 'firmware';
|
||||
|
||||
@ViewChild('dt') dt!: Table;
|
||||
|
||||
// Search functionality properties
|
||||
public firmwareSearch: string = '';
|
||||
|
|
@ -81,7 +74,6 @@ export class SettingsComponent implements OnInit {
|
|||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public SysConfigloading: boolean = true;
|
||||
|
||||
|
|
@ -104,37 +96,9 @@ export class SettingsComponent implements OnInit {
|
|||
closeButton: true,
|
||||
};
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.dt.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
public timezones = this.TimeZones.timezones;
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -208,9 +172,9 @@ export class SettingsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onSelectedRows(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRows(rows: any[]): void {
|
||||
this.rows = rows;
|
||||
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
this.Selectedrows = rows.map((m: any) => m.id);
|
||||
}
|
||||
|
||||
show_toast(title: string, body: string, color: string) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import {
|
|||
} from "@coreui/angular";
|
||||
import { SettingsRoutingModule } from "./settings-routing.module";
|
||||
import { SettingsComponent } from "./settings.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
|
|
@ -29,7 +31,9 @@ import { FormsModule } from "@angular/forms";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
SpinnerModule,
|
||||
ToastModule,
|
||||
ModalModule,
|
||||
|
|
|
|||
|
|
@ -8,46 +8,71 @@
|
|||
</c-col>
|
||||
<c-col xs [lg]="9">
|
||||
<h6 style="text-align: right;">
|
||||
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Snippet('','showadd')"
|
||||
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Snippet('','showadd')"
|
||||
style="color: #fff;"><i class="fa-solid fa-plus"></i> </button>
|
||||
</h6>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<gui-grid [source]="source" [searching]="searching" [paging]="paging" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[rowSelection]="rowSelection" (selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
|
||||
<i *ngIf="item.status=='updated'" style="color: green;margin: 5px;" class="fa-solid fa-check"></i>
|
||||
<i *ngIf="item.status=='failed'" style="color: red;margin: 5px;" class="fa-solid fa-x"></i>
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Description" field="description">
|
||||
<ng-template let-value="item.description" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Created" field="created">
|
||||
<ng-template let-value="item.created" let-item="item" let-index="index">
|
||||
<div>{{value}}</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" field="action" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="primary" size="sm" (click)="Edit_Snippet(item,'edit')" class=""><i
|
||||
class="fa-regular fa-pen-to-square mx-1"></i>Edit</button>
|
||||
<button cButton color="warning" size="sm" (click)="Run_Snippet(item,'exec')" class="mx-1"><i
|
||||
class="fa-solid fa-bolt mx-1"></i>Execute</button>
|
||||
<button cButton color="info" size="sm" (click)="show_exec(item)" class="mx-1"><i
|
||||
class="fa-solid fa-bolt mx-1"></i>Data</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)" class=""><i
|
||||
class="fa-regular fa-trash-can mx-1"></i>Delete</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[(selection)]="Selectedrows" [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true"
|
||||
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name','description']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
Name
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="description" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
Description
|
||||
<p-sortIcon field="description"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="created" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
Created
|
||||
<p-sortIcon field="created"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 280px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>
|
||||
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
|
||||
<i *ngIf="item.status=='updated'" style="color: green;margin-right: 5px;" class="fa-solid fa-check"></i>
|
||||
<i *ngIf="item.status=='failed'" style="color: red;margin-right: 5px;" class="fa-solid fa-x"></i>
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>{{item.description}}</td>
|
||||
<td class="small">{{item.created}}</td>
|
||||
<td class="text-center">
|
||||
<button cButton color="primary" size="sm" (click)="Edit_Snippet(item,'edit')" class="me-1">
|
||||
<i class="fa-regular fa-pen-to-square me-1"></i>Edit
|
||||
</button>
|
||||
<button cButton color="warning" size="sm" (click)="Run_Snippet(item,'exec')" class="me-1">
|
||||
<i class="fa-solid fa-bolt me-1"></i>Execute
|
||||
</button>
|
||||
<button cButton color="info" size="sm" (click)="show_exec(item)" class="me-1">
|
||||
<i class="fa-solid fa-history me-1"></i>History
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)">
|
||||
<i class="fa-regular fa-trash-can me-1"></i>Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
@ -63,7 +88,8 @@
|
|||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input cFormControl id="floatingInput" placeholder="current_snippet['name']" [(ngModel)]="current_snippet['name']" disabled="true"/>
|
||||
<input cFormControl id="floatingInput" placeholder="current_snippet['name']" [(ngModel)]="current_snippet['name']"
|
||||
disabled="true" />
|
||||
<label cLabel for="floatingInput">Snipet Name</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -84,30 +110,36 @@
|
|||
</c-input-group>
|
||||
|
||||
<h5>Members :</h5>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[infoPanel]="infoPanel" [rowSelection]="rowSelection" [autoResizeWidth]=true [paging]="paging">
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="MAC" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="danger" size="sm" (click)="remove_member(item)"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table #dtSelected [value]="SelectedMembers" [paginator]="true" [rows]="5" [resizableColumns]="true"
|
||||
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>Name <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="mac" pResizableColumn>MAC
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
</th>
|
||||
<th style="width: 80px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-member>
|
||||
<tr>
|
||||
<td>{{member.name}}</td>
|
||||
<td *ngIf="current_snippet['selection_type']=='devices'">{{member.mac}}</td>
|
||||
<td class="text-center">
|
||||
<button cButton color="danger" size="sm" (click)="remove_member(member)">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<hr />
|
||||
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
|
||||
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="submit('exec')" cButton color="primary">Execute</button>
|
||||
<button (click)="submit('exec')" cButton color="primary">Execute</button>
|
||||
<button [cModalToggle]="ExecSnipetModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
|
|
@ -123,25 +155,48 @@
|
|||
<c-modal-body>
|
||||
<c-input-group class="mb-3">
|
||||
<h5>Group Members :</h5>
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
|
||||
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true
|
||||
[paging]="paging">
|
||||
<gui-grid-column header="Member Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="current_snippet['selection_type']=='devices'" header="MAC Address" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end w-100">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search..." (input)="applyFilterNewMember($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
<p-table #dtNewMember [value]="availbleMembers" [paginator]="true" [rows]="10"
|
||||
[(selection)]="SelectedNewMemberRows" (selectionChange)="NewMemberRows = $event" [resizableColumns]="true"
|
||||
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
|
||||
Class="p-datatable-sm p-datatable-gridlines p-datatable-striped" [globalFilterFields]="['name','ip','mac']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 3rem" pResizableColumn>
|
||||
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
Name
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="ip" pResizableColumn>
|
||||
IP
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th *ngIf="current_snippet['selection_type']=='devices'" pSortableColumn="mac" pResizableColumn>
|
||||
MAC
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-member>
|
||||
<tr>
|
||||
<td><p-tableCheckbox [value]="member"></p-tableCheckbox></td>
|
||||
<td>{{member.name}}</td>
|
||||
<td *ngIf="current_snippet['selection_type']=='devices'">{{member.ip}}</td>
|
||||
<td *ngIf="current_snippet['selection_type']=='devices'">{{member.mac}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<br />
|
||||
</c-input-group>
|
||||
<hr />
|
||||
|
|
@ -164,25 +219,44 @@
|
|||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<c-input-group class="mb-3">
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching"
|
||||
[source]="ExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[autoResizeWidth]=true
|
||||
[paging]="paging">
|
||||
<gui-grid-column header="Start time" field="start">
|
||||
<ng-template let-value="item['started']" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="End time" field="end">
|
||||
<ng-template let-value="item['ended']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="info" field="mac" align="center">
|
||||
<ng-template let-value="item['result']" let-item="item" let-index="index">
|
||||
<button (click)="exportToCsv(value)" color="primary" cButton>download</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end w-100">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search history..." (input)="applyFilterHistory($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
<p-table #dtHistory width="100%" [value]="ExecutedData" [paginator]="true" [rows]="10" [resizableColumns]="true"
|
||||
columnResizeMode="expand" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['username','detail']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="started" pResizableColumn>
|
||||
Start time
|
||||
<p-sortIcon field="started"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="started" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="ended" pResizableColumn>
|
||||
End time
|
||||
<p-sortIcon field="ended"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ended" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th style="width: 120px" class="text-center" pResizableColumn>Info</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td class="small">{{item.started}}</td>
|
||||
<td class="small">{{item.ended}}</td>
|
||||
<td class="text-center">
|
||||
<button (click)="exportToCsv(item.result)" color="primary" pTooltip="Download CSV" size="sm" cButton>
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<br />
|
||||
</c-input-group>
|
||||
<hr />
|
||||
|
|
@ -210,16 +284,19 @@
|
|||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input cFormControl id="floatingInput" placeholder="Snippet Description" [(ngModel)]="current_snippet['description']" />
|
||||
<input cFormControl id="floatingInput" placeholder="Snippet Description"
|
||||
[(ngModel)]="current_snippet['description']" />
|
||||
<label cLabel for="floatingInput">Description</label>
|
||||
</div>
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<textarea [style.height.px]="50 + (23 * lineNum)"
|
||||
cFormControl (ngModelChange)="calcline($event)" id="floatingInput" placeholder="Snippet code" [(ngModel)]="current_snippet['content']" ></textarea>
|
||||
<textarea [style.height.px]="50 + (23 * lineNum)" cFormControl (ngModelChange)="calcline($event)"
|
||||
id="floatingInput" placeholder="Snippet code" [(ngModel)]="current_snippet['content']"></textarea>
|
||||
<label cLabel for="floatingInput">Code</label>
|
||||
<div class="col-sm-12 c-d-block c-text-truncate">Note : In case of multiple IP addresses for the MikroWizard server, use<code style="padding: 0!important;">[mikrowizard]</code> instead of the MikroWizard server IP.</div>
|
||||
<div class="col-sm-12 c-d-block c-text-truncate">Note : In case of multiple IP addresses for the MikroWizard
|
||||
server, use<code style="padding: 0!important;">[mikrowizard]</code> instead of the MikroWizard server IP.
|
||||
</div>
|
||||
</div>
|
||||
</c-input-group>
|
||||
<br />
|
||||
|
|
@ -230,27 +307,28 @@
|
|||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible"
|
||||
id="DeleteConfirmModal">
|
||||
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Confirm delete {{ SelectedSnippet['name'] }}</h5>
|
||||
<h5 cModalTitle>Confirm delete {{ SelectedSnippet['name'] }}</h5>
|
||||
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
Are you sure that You want to delete following Snippet ?
|
||||
<br/>
|
||||
<br/>
|
||||
<br />
|
||||
<br />
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td><b>Snippet name : </b>{{ SelectedSnippet['name'] }}
|
||||
</tr>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p ><code style="padding: 0!important;"><b>Warning:</b> ALL <b>Tasks</b> related to this snippet Will be <b>modifed or deleted</b> and stop working!</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Snippet name : </b>{{ SelectedSnippet['name'] }}
|
||||
</tr>
|
||||
<tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p><code
|
||||
style="padding: 0!important;"><b>Warning:</b> ALL <b>Tasks</b> related to this snippet Will be <b>modifed or deleted</b> and stop working!</code>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
|
|
@ -263,6 +341,4 @@
|
|||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
|
|
@ -12,32 +12,19 @@ import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
|
|||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import { HttpClient, HttpClientModule, HttpParams } from "@angular/common/http";
|
||||
import {
|
||||
GuiCellView,
|
||||
GuiSearching,
|
||||
GuiSelectedRow,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiDataType,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowColoring,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
|
||||
@Component({
|
||||
templateUrl: "snippets.component.html",
|
||||
})
|
||||
export class SnippetsComponent implements OnInit, OnDestroy {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public tz: string;
|
||||
public uid!: number;
|
||||
public uname!: string;
|
||||
public tz!: string;
|
||||
public ispro: boolean = false;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
|
|
@ -56,7 +43,8 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
// console.dir("res",res)
|
||||
_self.uid = res.uid;
|
||||
_self.uname = res.name;
|
||||
_self.tz = res.tz;
|
||||
_self.tz = res.tz;
|
||||
_self.ispro = res.ispro;
|
||||
// console.dir("role",res.role);
|
||||
const userId = _self.uid;
|
||||
|
||||
|
|
@ -73,9 +61,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
@ViewChild("nameSummaryCell")
|
||||
nameSummaryCell: TemplateRef<any>;
|
||||
nameSummaryCell!: TemplateRef<any>;
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public columns: Array<any> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
|
|
@ -94,6 +82,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
public NewMemberRows: any = [];
|
||||
public SelectedNewMemberRows: any;
|
||||
|
||||
@ViewChild('dtNewMember') dtNewMember!: Table;
|
||||
@ViewChild('dtHistory') dtHistory!: Table;
|
||||
|
||||
public current_snippet: any = {
|
||||
content: "",
|
||||
created: "",
|
||||
|
|
@ -110,44 +101,15 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
name: "",
|
||||
};
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
|
||||
public ip_scanner: any;
|
||||
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
applyFilterNewMember($event: any, stringVal: string) {
|
||||
this.dtNewMember.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterHistory($event: any, stringVal: string) {
|
||||
this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGridTable();
|
||||
|
|
@ -182,31 +144,31 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
this.ModalAction = "edit";
|
||||
}
|
||||
}
|
||||
show_exec(item:any){
|
||||
var _self=this;
|
||||
this.SelectedSnippet = item;
|
||||
this.ExecutedDataModalVisible = true;
|
||||
this.data_provider
|
||||
.get_executed_snipet(_self.SelectedSnippet["id"])
|
||||
.then((res) => {
|
||||
let index = 1;
|
||||
_self.ExecutedData= res.map((d: any) => {
|
||||
d.index = index;
|
||||
d.ended = formatInTimeZone(
|
||||
d.created.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
d.started = formatInTimeZone(
|
||||
d.info.created.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
index += 1;
|
||||
return d;
|
||||
});
|
||||
_self.DeleteConfirmModalVisible = false;
|
||||
});
|
||||
show_exec(item: any) {
|
||||
var _self = this;
|
||||
this.SelectedSnippet = item;
|
||||
this.ExecutedDataModalVisible = true;
|
||||
this.data_provider
|
||||
.get_executed_snipet(_self.SelectedSnippet["id"])
|
||||
.then((res) => {
|
||||
let index = 1;
|
||||
_self.ExecutedData = res.map((d: any) => {
|
||||
d.index = index;
|
||||
d.ended = formatInTimeZone(
|
||||
d.created.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
d.started = formatInTimeZone(
|
||||
d.info.created.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
index += 1;
|
||||
return d;
|
||||
});
|
||||
_self.DeleteConfirmModalVisible = false;
|
||||
});
|
||||
}
|
||||
|
||||
form_changed() {
|
||||
|
|
@ -254,11 +216,11 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRowsNewMembers(rows: Array<any>): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
|
||||
this.SelectedNewMemberRows = rows.map((m: any) => m.source || m);
|
||||
}
|
||||
|
||||
|
||||
add_new_members() {
|
||||
var _self = this;
|
||||
_self.SelectedMembers = [
|
||||
|
|
@ -274,12 +236,12 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
|
||||
submit(action: string) {
|
||||
var _self = this;
|
||||
this.data_provider
|
||||
.Exec_snipet(_self.current_snippet, _self.SelectedTaskItems)
|
||||
.then((res) => {
|
||||
_self.initGridTable();
|
||||
});
|
||||
|
||||
this.data_provider
|
||||
.Exec_snipet(_self.current_snippet, _self.SelectedTaskItems)
|
||||
.then((res) => {
|
||||
_self.initGridTable();
|
||||
});
|
||||
|
||||
this.ExecSnipetModalVisible = false;
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +249,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
this.current_snippet = item;
|
||||
this.current_snippet["task_type"] = "snipet_exec";
|
||||
this.current_snippet["selection_type"] = "devices";
|
||||
this.form_changed();
|
||||
this.form_changed();
|
||||
this.ExecSnipetModalVisible = true;
|
||||
this.ModalAction = "exec";
|
||||
}
|
||||
|
|
@ -303,9 +265,9 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
onSelectedRows(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRows(rows: Array<any>): void {
|
||||
this.rows = rows;
|
||||
this.Selectedrows = rows.map((m: GuiSelectedRow) => m.source.id);
|
||||
this.Selectedrows = rows.map((m: any) => (m.source ? m.source.id : m.id));
|
||||
}
|
||||
|
||||
remove(item: any) {
|
||||
|
|
@ -318,7 +280,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
|
||||
initGridTable(): void {
|
||||
var _self = this;
|
||||
_self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => {
|
||||
_self.data_provider.get_snippets("", "", "", 0, 1000, false).then((res) => {
|
||||
_self.source = res.map((x: any) => {
|
||||
x.created = [
|
||||
x.created.split("T")[0],
|
||||
|
|
@ -330,18 +292,18 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
sanitizeString(desc:string) {
|
||||
var itemDesc:string='';
|
||||
sanitizeString(desc: string) {
|
||||
var itemDesc: string = '';
|
||||
if (desc) {
|
||||
itemDesc = desc.toString().replace(/"/g, '\"');
|
||||
itemDesc = itemDesc.replace(/'/g, '\'');
|
||||
itemDesc = desc.toString().replace(/"/g, '\"');
|
||||
itemDesc = itemDesc.replace(/'/g, '\'');
|
||||
} else {
|
||||
itemDesc = '';
|
||||
itemDesc = '';
|
||||
}
|
||||
return itemDesc;
|
||||
}
|
||||
|
||||
exportToCsv(jsonResponse:any) {
|
||||
exportToCsv(jsonResponse: any) {
|
||||
const data = jsonResponse;
|
||||
const columns = this.getColumns(data);
|
||||
const csvData = this.convertToCsv(data, columns);
|
||||
|
|
@ -349,7 +311,7 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
getColumns(data: any[]): string[] {
|
||||
const columns : any = [];
|
||||
const columns: any = [];
|
||||
data.forEach(row => {
|
||||
Object.keys(row).forEach((col) => {
|
||||
if (!columns.includes(col)) {
|
||||
|
|
@ -361,13 +323,13 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
convertToCsv(data: any[], columns: string[]): string {
|
||||
var _self=this;
|
||||
var _self = this;
|
||||
let csv = '';
|
||||
csv += columns.join(',') + '\n';
|
||||
data.forEach(row => {
|
||||
const values : any = [];
|
||||
columns.forEach((col:any) => {
|
||||
values.push('"'+_self.sanitizeString(row[col])+'"');
|
||||
const values: any = [];
|
||||
columns.forEach((col: any) => {
|
||||
values.push('"' + _self.sanitizeString(row[col]) + '"');
|
||||
});
|
||||
csv += values.join(',') + '\n';
|
||||
});
|
||||
|
|
@ -376,10 +338,10 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
|
||||
downloadFile(data: string, filename: string, type: string) {
|
||||
const blob = new Blob([data], { type: type });
|
||||
const nav = (window.navigator as any);
|
||||
const nav = (window.navigator as any);
|
||||
|
||||
if (nav.msSaveOrOpenBlob) {
|
||||
nav.msSaveBlob(blob, filename);
|
||||
nav.msSaveBlob(blob, filename);
|
||||
} else {
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', URL.createObjectURL(blob));
|
||||
|
|
@ -405,5 +367,5 @@ export class SnippetsComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
ngOnDestroy(): void { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import {
|
|||
} from "@coreui/angular";
|
||||
import { SnippetsRoutingModule } from "./snippets-routing.module";
|
||||
import { SnippetsComponent } from "./snippets.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -24,7 +26,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
ModalModule,
|
||||
ToastModule,
|
||||
FormsModule,
|
||||
|
|
|
|||
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 {
|
||||
}
|
||||
571
src/app/views/syslog-regex/syslog-regex.component.html
Normal file
571
src/app/views/syslog-regex/syslog-regex.component.html
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
<div style="position: relative;">
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4">
|
||||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="3">
|
||||
Syslog Custom Regex
|
||||
</c-col>
|
||||
<c-col xs [lg]="9">
|
||||
<h6 style="text-align: right;">
|
||||
<button cButton color="dark" class="mx-1" size="sm" (click)="Edit_Regex('','add')"
|
||||
style="color: #fff;"><i class="fa-solid fa-plus"></i> Add New Regex</button>
|
||||
<button cButton color="warning" class="mx-1" size="sm" (click)="openManageAlerts()"
|
||||
style="color: #000;"><i class="fa-solid fa-bell"></i> Manage Alerts</button>
|
||||
</h6>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search regexes..."
|
||||
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt showGridlines [value]="source" [paginator]="true" [rows]="10"
|
||||
[showCurrentPageReport]="true" [rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true"
|
||||
columnResizeMode="expand" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name','regex_pattern','match_string']">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
Name
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="regex_pattern" pResizableColumn>
|
||||
Regex Pattern
|
||||
<p-sortIcon field="regex_pattern"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="regex_pattern" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="alert_id" pResizableColumn>Alert / Severity <p-sortIcon
|
||||
field="alert_id"></p-sortIcon></th>
|
||||
<th pSortableColumn="alert_enabled" pResizableColumn
|
||||
style="width: 15rem; text-align: center;">Alert Enabled/Type <p-sortIcon
|
||||
field="alert_enabled"></p-sortIcon></th>
|
||||
<th pSortableColumn="match_string" pResizableColumn>
|
||||
Match String
|
||||
<p-sortIcon field="match_string"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="match_string" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="created" pResizableColumn>Created <p-sortIcon
|
||||
field="created"></p-sortIcon></th>
|
||||
<th style="width: 10rem; text-align: center;" class="p-resizable-column">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>{{item.name}}</td>
|
||||
<td><code class="px-1 py-1">{{item.regex_pattern}}</code></td>
|
||||
<td>
|
||||
<div class="d-flex flex-row gap-1 align-items-center">
|
||||
<span class="fw-bold">{{ getAlertName(item.alert_id) }}</span>
|
||||
<c-badge size="sm"
|
||||
[color]="getAlertLevel(item.alert_id).toLowerCase() =='critical' ? 'danger' :
|
||||
getAlertLevel(item.alert_id).toLowerCase() =='warning' ? 'warning' :
|
||||
getAlertLevel(item.alert_id).toLowerCase() =='info' ? 'info' :
|
||||
getAlertLevel(item.alert_id).toLowerCase() =='error' ? 'danger' : 'secondary'">{{
|
||||
getAlertLevel(item.alert_id)}}</c-badge>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<i *ngIf="item.alert_enabled" class="fa-solid fa-check mx-1" style="color: green;"></i>
|
||||
<i *ngIf="!item.alert_enabled" class="fa-solid fa-x mx-1" style="color: red;"></i>
|
||||
<span *ngIf="item.alert_enabled && item.global_alert"><i
|
||||
class="fa-solid fa-earth-americas text-primary" title="Global Alert"></i>
|
||||
Global</span>
|
||||
<span *ngIf="item.alert_enabled && !item.global_alert"><i
|
||||
class="fa-solid fa-server text-secondary" title="Device Specific"></i> String
|
||||
Match</span>
|
||||
</td>
|
||||
<td>{{item.match_string}}</td>
|
||||
<td>{{item.created}}</td>
|
||||
<td class="text-center">
|
||||
<button cButton color="primary" size="sm" (click)="Edit_Regex(item,'edit')" class="mx-1"
|
||||
title="Edit">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item,false)"
|
||||
class="mx-1" title="Delete">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="7">No regexes found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
|
||||
<c-modal #EditRegexModal backdrop="static" size="xl" [fullscreen]="true" [(visible)]="EditRegexModalVisible"
|
||||
id="EditRegexModal">
|
||||
<c-modal-header class="bg-light">
|
||||
<h5 *ngIf="ModalAction=='edit'" cModalTitle>
|
||||
<i class="fa-solid fa-edit me-2"></i>Edit Regex: {{current_regex['name']}}
|
||||
</h5>
|
||||
<h5 *ngIf="ModalAction=='add'" cModalTitle>
|
||||
<i class="fa-solid fa-plus me-2"></i>Add New Regex
|
||||
</h5>
|
||||
<button (click)="EditRegexModalVisible=false" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-4 bg-light">
|
||||
<c-card class="mb-4 shadow-sm border-0">
|
||||
<c-card-header class="bg-white border-bottom-0 pt-3 pb-0">
|
||||
<h6 class="mb-1"><i class="fa-solid fa-code text-primary me-2"></i>Configuration Details</h6>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- LEFT COLUMN: Info & Configuration -->
|
||||
<div class="col-lg-5">
|
||||
<!-- SECTION 1: Basic Info & Log Input -->
|
||||
<div class="mb-4">
|
||||
<h6 class="border-bottom pb-2 mb-3 text-primary"><i
|
||||
class="fa-solid fa-info-circle me-2"></i>1. General Information</h6>
|
||||
|
||||
<div [cFormFloating]="true" class="mb-4">
|
||||
<input cFormControl placeholder="e.g. Failed VPN Auth" [(ngModel)]="current_regex.name"
|
||||
id="regexName" class="border-secondary" />
|
||||
<label cLabel for="regexName">Regex Name</label>
|
||||
</div>
|
||||
|
||||
<label class="form-label fw-bold small text-muted mb-1">Live Syslog Sample</label>
|
||||
<p class="small text-muted mb-2">Provide a real log message to build and test your regex
|
||||
against.</p>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-5">
|
||||
<select class="form-select border-secondary shadow-sm bg-light"
|
||||
[(ngModel)]="selectedSampleIndex" (ngModelChange)="onSampleSelected()">
|
||||
<option [ngValue]="null" disabled *ngIf="syslogSamples.length > 0">-- Pick from
|
||||
recent logs --</option>
|
||||
<option [ngValue]="null" disabled *ngIf="syslogSamples.length === 0">No recent
|
||||
samples</option>
|
||||
<option *ngFor="let sample of syslogSamples; let i = index" [ngValue]="i"
|
||||
[title]="sample">
|
||||
{{ (sample.length > 50) ? (sample | slice:0:50) + '...' : sample }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-7 position-relative">
|
||||
<textarea class="form-control border-secondary shadow-sm" rows="2"
|
||||
[(ngModel)]="sampleLog"
|
||||
(ngModelChange)="evaluateRegexAgainstSample(); updateBuilder()"
|
||||
placeholder="Paste mock log here..."></textarea>
|
||||
<div class="backdrop" [innerHTML]="getHighlightedSampleBoxHtml()"
|
||||
style="padding: 0.375rem 0.75rem; font-size: 0.875rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 2: Goal / Alert Configuration -->
|
||||
<div class="mb-4">
|
||||
<h6 class="border-bottom pb-2 mb-3 text-primary"><i class="fa-solid fa-bullseye me-2"></i>2.
|
||||
Event Handling & Alerts</h6>
|
||||
<p class="small text-muted mb-3">What happens when this regex matches a log? The event will
|
||||
inherit the title and severity from the chosen Alert.</p>
|
||||
|
||||
<div class="card bg-light border-secondary shadow-sm">
|
||||
<div class="card-body py-3">
|
||||
<c-form-check class="mb-3">
|
||||
<input cFormCheckInput type="checkbox" id="alertEnabled"
|
||||
[(ngModel)]="current_regex.alert_enabled"
|
||||
(ngModelChange)="evaluateRegexAgainstSample()" />
|
||||
<label cFormCheckLabel for="alertEnabled" class="fw-bold">Attach to Alert
|
||||
Definition</label>
|
||||
</c-form-check>
|
||||
|
||||
<div *ngIf="current_regex.alert_enabled" class="border-top pt-3 mt-2">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-bold mb-1">Alert
|
||||
Definition</label>
|
||||
<div class="d-flex">
|
||||
<select class="form-select border-secondary"
|
||||
[(ngModel)]="current_regex.alert_id"
|
||||
(ngModelChange)="evaluateRegexAgainstSample()">
|
||||
<option [ngValue]="null" disabled>Select Alert...</option>
|
||||
<option *ngFor="let a of Alerts" [value]="a.id">{{a.name}}
|
||||
({{a.level}})</option>
|
||||
</select>
|
||||
<button cButton color="warning" variant="outline" class="ms-2"
|
||||
(click)="openManageAlerts()" title="Manage Alerts"><i
|
||||
class="fa-solid fa-cog"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-bold mb-1">Storage
|
||||
Condition</label>
|
||||
<select class="form-select border-secondary"
|
||||
[(ngModel)]="current_regex.alert_mode">
|
||||
<option value="global">Always Store (Global)</option>
|
||||
<option value="conditional">Only if String Matches (Conditional)
|
||||
</option>
|
||||
</select>
|
||||
<div *ngIf="current_regex.alert_mode === 'conditional'" class="mt-2">
|
||||
<input cFormControl placeholder="e.g. error"
|
||||
[(ngModel)]="current_regex.match_string"
|
||||
class="border-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 border-top pt-3" *ngIf="!current_regex.alert_enabled">
|
||||
<small class="text-danger d-block"><i
|
||||
class="fa-solid fa-triangle-exclamation me-1"></i>Regex matching will
|
||||
occur, but no alert,soring in db or action will be
|
||||
applied.</small>
|
||||
</div>
|
||||
|
||||
<!-- Fallbacks -->
|
||||
<div class="row g-3 mt-2 border-top pt-3" *ngIf="current_regex.alert_enabled">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-bold mb-1">Default Event
|
||||
Type</label>
|
||||
<select class="form-select form-select-sm border-secondary"
|
||||
[(ngModel)]="current_regex.eventtype"
|
||||
(ngModelChange)="evaluateRegexAgainstSample()">
|
||||
<option value="">-- Auto --</option>
|
||||
<option value="state">state</option>
|
||||
<option value="connection">connection</option>
|
||||
<option value="config">config</option>
|
||||
<option value="firmware">firmware</option>
|
||||
<option value="health">health</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-bold mb-1">Default
|
||||
Status</label>
|
||||
<select class="form-select form-select-sm border-secondary"
|
||||
[(ngModel)]="current_regex.status"
|
||||
(ngModelChange)="evaluateRegexAgainstSample()">
|
||||
<option [ngValue]="0">0 - Open (not fixed)</option>
|
||||
<option [ngValue]="1">1 - Fixed (resolved)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- END LEFT COLUMN -->
|
||||
|
||||
<!-- RIGHT COLUMN: Builder & Preview -->
|
||||
<div class="col-lg-7 border-start ps-4">
|
||||
|
||||
<!-- SECTION 3: Regex Builder -->
|
||||
<div class="mb-4">
|
||||
<h6 class="border-bottom pb-2 mb-3 text-primary"><i class="fa-solid fa-code me-2"></i>3.
|
||||
Pattern Extraction</h6>
|
||||
|
||||
<ul class="nav nav-tabs nav-justified mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link cursor-pointer text-dark fw-bold"
|
||||
[class.active]="inputMode === 'builder'" (click)="setMode('builder')">
|
||||
<i class="fa-solid fa-wand-magic-sparkles me-2 text-primary"></i>Visual Builder
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link cursor-pointer text-dark fw-bold"
|
||||
[class.active]="inputMode === 'raw'" (click)="setMode('raw')">
|
||||
<i class="fa-solid fa-terminal me-2 text-danger"></i>Raw Regex
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item ms-auto" *ngIf="sampleLog">
|
||||
<button cButton color="info" variant="outline" size="sm"
|
||||
class="fw-bold mt-1 animate-fade-in" (click)="openAIHelp()"
|
||||
title="Ask AI for help with regex">
|
||||
<i class="fa-solid fa-robot me-1"></i> AI Help
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- RAW MODE -->
|
||||
<div *ngIf="inputMode === 'raw'" class="p-3 border border-top-0 rounded-bottom bg-light">
|
||||
<div [cFormFloating]="true" class="mb-1">
|
||||
<input cFormControl placeholder="e.g. vpn_error: (.*)"
|
||||
[(ngModel)]="current_regex.regex_pattern" (ngModelChange)="onRawRegexChange()"
|
||||
id="regexPattern" class="border-secondary font-monospace"
|
||||
[class.is-invalid]="rawRegexError" />
|
||||
<label cLabel for="regexPattern">Python Regex Pattern</label>
|
||||
</div>
|
||||
<small *ngIf="rawRegexError" class="text-danger d-block mb-2"><i
|
||||
class="fa-solid fa-triangle-exclamation me-1"></i>{{rawRegexError}}</small>
|
||||
<small class="text-muted"><i
|
||||
class="fa-solid fa-circle-info me-1 mt-1 text-primary"></i>Only the
|
||||
<code class="px-1 py-1">(?P<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>
|
||||
125
src/app/views/syslog-regex/syslog-regex.component.scss
Normal file
125
src/app/views/syslog-regex/syslog-regex.component.scss
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
.alert-config-box {
|
||||
transition: all 0.3s ease;
|
||||
border-color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
color: #d63384;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
/* Regex Builder Styles */
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Highlighter Textarea Overlay Trick */
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: transparent !important;
|
||||
color: transparent !important;
|
||||
/* Hide real text to only show highlights behind it, but keep caret visible */
|
||||
caret-color: #000;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
padding: 0.375rem 0.75rem;
|
||||
/* Match textarea padding */
|
||||
font-size: 0.8rem;
|
||||
/* Match small text if needed */
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
border: 1px solid transparent;
|
||||
pointer-events: none;
|
||||
border-radius: 0.375rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.segment-row {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid #edf2f7 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffffff !important;
|
||||
border-color: #90cdf4 !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .btn-link {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.test-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.step-card {
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
--cui-table-bg: transparent;
|
||||
--cui-table-color: #fff;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
590
src/app/views/syslog-regex/syslog-regex.component.ts
Normal file
590
src/app/views/syslog-regex/syslog-regex.component.ts
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Table } from 'primeng/table';
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { ToastComponent } from '@coreui/angular';
|
||||
|
||||
interface RegexSegment {
|
||||
type: 'static' | 'dynamic';
|
||||
value: string; // Used for static text
|
||||
captureType?: string; // Used for dynamic: everything, word, ip, mac
|
||||
targetField?: string; // Mapping: src, detail, level, status, comment
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-syslog-regex',
|
||||
templateUrl: './syslog-regex.component.html',
|
||||
styleUrls: ['./syslog-regex.component.scss']
|
||||
})
|
||||
export class SyslogRegexComponent implements OnInit {
|
||||
@ViewChild('dt') dt: Table | undefined;
|
||||
|
||||
public syslogRegexes: any[] = [];
|
||||
|
||||
// Grid config
|
||||
source: Array<any> = [];
|
||||
searching = {
|
||||
enabled: true,
|
||||
placeholder: 'Search regexes...'
|
||||
};
|
||||
paging = {
|
||||
enabled: true,
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 25, 50]
|
||||
};
|
||||
columnMenu = { enabled: false };
|
||||
sorting = { enabled: true };
|
||||
infoPanel = { enabled: true };
|
||||
rowSelection: any = {
|
||||
enabled: true,
|
||||
type: 'checkbox',
|
||||
mode: 'multiple',
|
||||
};
|
||||
|
||||
public EditRegexModalVisible: boolean = false;
|
||||
public current_regex: any = {
|
||||
id: 0,
|
||||
name: '',
|
||||
regex_pattern: '',
|
||||
alert_enabled: false,
|
||||
alert_id: null,
|
||||
global_alert: true,
|
||||
match_string: '',
|
||||
alert_mode: 'global',
|
||||
default_eventtype: '',
|
||||
default_status: 0
|
||||
};
|
||||
|
||||
// Builder vs Raw Mode
|
||||
public inputMode: 'raw' | 'builder' = 'raw';
|
||||
public rawRegexError: string = '';
|
||||
public sampleLog: string = '';
|
||||
|
||||
// Segment-based Builder State
|
||||
public segments: RegexSegment[] = [];
|
||||
|
||||
public generatedRegex: string = '';
|
||||
public testFeedback: string = '';
|
||||
public testResults: { field: string, value: string }[] = [];
|
||||
public simulatedDbRow: { eventtype: string, src: string, detail: string, level: string, status: string, comment: string } | null = null;
|
||||
public isTestValid: boolean = false;
|
||||
public extractedValue: string = '';
|
||||
public syslogSamples: string[] = [];
|
||||
public selectedSampleIndex: number | null = null;
|
||||
|
||||
public ManageAlertsModalVisible: boolean = false;
|
||||
public editingAlert: any = { id: 0, name: '', level: 'Info' };
|
||||
|
||||
public Alerts: any = [];
|
||||
public ModalAction: string = "add";
|
||||
|
||||
// super select options
|
||||
alertOptions: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Select Alert",
|
||||
searchEnabled: true,
|
||||
};
|
||||
|
||||
constructor(public MikroWizardRPC: dataProvider) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadRegexes();
|
||||
this.loadAlerts();
|
||||
}
|
||||
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.dt!.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
loadRegexes() {
|
||||
this.MikroWizardRPC.get_syslog_regexes().then(
|
||||
(res: any) => {
|
||||
if (res) {
|
||||
this.source = res;
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
// Handle error visually if needed
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadAlerts(): void {
|
||||
this.MikroWizardRPC.get_alerts().then((res: any) => {
|
||||
this.Alerts = res.map((x: any) => ({ id: x.id, name: x.name, level: x.level }));
|
||||
});
|
||||
}
|
||||
|
||||
resetBuilder() {
|
||||
this.segments = [];
|
||||
this.generatedRegex = '';
|
||||
this.testFeedback = '';
|
||||
this.testResults = [];
|
||||
this.simulatedDbRow = null;
|
||||
this.extractedValue = '';
|
||||
this.selectedSampleIndex = null;
|
||||
this.loadSyslogSamples();
|
||||
}
|
||||
|
||||
Edit_Regex(item: any, mode: string) {
|
||||
// Reset builder state
|
||||
this.inputMode = 'raw';
|
||||
this.rawRegexError = '';
|
||||
this.resetBuilder();
|
||||
|
||||
if (mode === 'add') {
|
||||
this.current_regex = {
|
||||
id: 0,
|
||||
name: '',
|
||||
regex_pattern: '',
|
||||
alert_enabled: false,
|
||||
alert_id: null,
|
||||
global_alert: true,
|
||||
match_string: '',
|
||||
alert_mode: 'global',
|
||||
eventtype: '',
|
||||
status: 0
|
||||
};
|
||||
this.ModalAction = 'add';
|
||||
} else {
|
||||
// deep copy
|
||||
this.current_regex = JSON.parse(JSON.stringify(item));
|
||||
this.current_regex.alert_mode = this.current_regex.global_alert ? 'global' : 'conditional';
|
||||
|
||||
// Try to deconstruct the regex into builder segments
|
||||
if (this.current_regex.regex_pattern) {
|
||||
const wasDeconstructed = this.deconstructRegex(this.current_regex.regex_pattern);
|
||||
if (wasDeconstructed) {
|
||||
this.inputMode = 'builder';
|
||||
}
|
||||
}
|
||||
|
||||
this.ModalAction = 'edit';
|
||||
}
|
||||
|
||||
// Initial validation
|
||||
this.validateRawRegex();
|
||||
|
||||
this.EditRegexModalVisible = true;
|
||||
}
|
||||
|
||||
save_regex() {
|
||||
if (this.inputMode === 'builder') {
|
||||
this.current_regex.regex_pattern = this.generatedRegex;
|
||||
}
|
||||
|
||||
// Final validation check
|
||||
this.validateRawRegex();
|
||||
if (this.rawRegexError && this.inputMode === 'raw') {
|
||||
return; // Prevent save if invalid
|
||||
}
|
||||
if (!this.isTestValid && this.inputMode === 'builder') {
|
||||
if (!this.generatedRegex) return;
|
||||
}
|
||||
|
||||
let payload = { ...this.current_regex };
|
||||
// Ensure alert_id is null if not alert_enabled
|
||||
if (!payload.alert_enabled) {
|
||||
payload.alert_id = null;
|
||||
payload.global_alert = false;
|
||||
payload.match_string = '';
|
||||
} else {
|
||||
if (payload.alert_mode === 'global') {
|
||||
payload.global_alert = true;
|
||||
payload.match_string = '';
|
||||
} else {
|
||||
payload.global_alert = false;
|
||||
if (!payload.match_string) {
|
||||
alert("Please provide a Match String for conditional alert storage.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove internal properties and level (now inherited from alert)
|
||||
delete payload.alert_mode;
|
||||
delete payload.level;
|
||||
|
||||
// Ensure alert_id is mandatory for save (as per v3.2)
|
||||
if (payload.alert_enabled && !payload.alert_id) {
|
||||
alert("Please select an Alert Definition.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.MikroWizardRPC.save_syslog_regex(payload).then(() => {
|
||||
this.EditRegexModalVisible = false;
|
||||
this.loadRegexes();
|
||||
});
|
||||
}
|
||||
|
||||
confirm_delete(item: any, confirm: boolean) {
|
||||
if (!confirm) {
|
||||
if (window.confirm("Are you sure you want to delete custom regex: " + item.name + "?")) {
|
||||
this.MikroWizardRPC.delete_syslog_regex(item.id).then(() => {
|
||||
this.loadRegexes();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Alert Management ---
|
||||
openManageAlerts() {
|
||||
this.ManageAlertsModalVisible = true;
|
||||
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
|
||||
}
|
||||
|
||||
editAlert(item: any) {
|
||||
this.editingAlert = { ...item };
|
||||
}
|
||||
|
||||
saveAlert() {
|
||||
this.MikroWizardRPC.save_alert(this.editingAlert).then(() => {
|
||||
this.loadAlerts();
|
||||
this.editingAlert = { id: 0, name: '', level: 'Info', description: '' };
|
||||
this.loadRegexes();
|
||||
});
|
||||
}
|
||||
|
||||
deleteAlert(id: number) {
|
||||
if (confirm("Are you sure you want to delete this alert?")) {
|
||||
this.MikroWizardRPC.delete_alert(id).then(() => {
|
||||
this.loadAlerts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Syslog Samples Management ---
|
||||
loadSyslogSamples() {
|
||||
this.MikroWizardRPC.get_syslogregex_samples().then(
|
||||
(res: any) => {
|
||||
if (res && Array.isArray(res)) {
|
||||
this.syslogSamples = res;
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
console.error("Failed to load syslog samples", error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onSampleSelected() {
|
||||
if (this.selectedSampleIndex !== null && this.syslogSamples[this.selectedSampleIndex]) {
|
||||
this.sampleLog = this.syslogSamples[this.selectedSampleIndex];
|
||||
this.updateBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
getAlertName(id: any): string {
|
||||
const alert = this.Alerts.find((a: any) => Number(a.id) === Number(id));
|
||||
return alert ? alert.name : 'No Alert';
|
||||
}
|
||||
|
||||
getAlertLevel(id: any): string {
|
||||
const alert = this.Alerts.find((a: any) => Number(a.id) === Number(id));
|
||||
return alert ? alert.level : 'N/A';
|
||||
}
|
||||
|
||||
openAIHelp() {
|
||||
if (!this.sampleLog) return;
|
||||
|
||||
const currentRegexText = this.current_regex.regex_pattern ? `\nOptional context - my current attempt is:\n"${this.current_regex.regex_pattern}"` : "";
|
||||
|
||||
const prompt = `I need help creating a Python regex pattern (re module syntax) for a specific syslog message.
|
||||
|
||||
### My Syslog Sample:
|
||||
"${this.sampleLog}"
|
||||
|
||||
### Requirement:
|
||||
Create a regex that extracts the most important information from this log into a named capture group called "comment" using the syntax: (?P<comment>...)
|
||||
${currentRegexText}
|
||||
|
||||
### Example Format:
|
||||
If the log was "System error: Disk full", the regex should be "System error: (?P<comment>.*)".
|
||||
|
||||
Please provide the completed Python regex for my sample log.`;
|
||||
|
||||
const encodedPrompt = encodeURIComponent(prompt);
|
||||
window.open(`https://chatgpt.com/?q=${encodedPrompt}`, '_blank');
|
||||
}
|
||||
|
||||
// --- Segment Management ---
|
||||
addSegment(type: 'static' | 'dynamic') {
|
||||
if (type === 'static') {
|
||||
this.segments.push({ type: 'static', value: '' });
|
||||
} else {
|
||||
this.segments.push({ type: 'dynamic', value: '', captureType: 'everything' });
|
||||
}
|
||||
this.updateBuilder();
|
||||
}
|
||||
|
||||
removeSegment(index: number) {
|
||||
this.segments.splice(index, 1);
|
||||
this.updateBuilder();
|
||||
}
|
||||
|
||||
moveSegmentUp(index: number) {
|
||||
if (index > 0) {
|
||||
const temp = this.segments[index - 1];
|
||||
this.segments[index - 1] = this.segments[index];
|
||||
this.segments[index] = temp;
|
||||
this.updateBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
moveSegmentDown(index: number) {
|
||||
if (index < this.segments.length - 1) {
|
||||
const temp = this.segments[index + 1];
|
||||
this.segments[index + 1] = this.segments[index];
|
||||
this.segments[index] = temp;
|
||||
this.updateBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Regex Builder Deconstruction ---
|
||||
deconstructRegex(pattern: string): boolean {
|
||||
// This is a specialized parser to turn regex strings back into segments
|
||||
if (!pattern) return false;
|
||||
|
||||
// Common patterns we use
|
||||
const capturePatterns: { [key: string]: string } = {
|
||||
'word': '\\S+',
|
||||
'ip': '\\d{1,3}(?:\\.\\d{1,3}){3}',
|
||||
'mac': '[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}',
|
||||
'number': '\\d+',
|
||||
'everything': '.*'
|
||||
};
|
||||
|
||||
try {
|
||||
// Split by the delimiter we use in updateBuilder: "\\s*"
|
||||
const parts = pattern.split('\\s*');
|
||||
const newSegments: any[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part) continue;
|
||||
|
||||
// Match a named capture group: (?P<name>pattern)
|
||||
const namedMatch = part.match(/^\(\?P<([^>]+)>(.+)\)$/);
|
||||
if (namedMatch) {
|
||||
const field = namedMatch[1];
|
||||
const innerPattern = namedMatch[2];
|
||||
|
||||
// Try to identify the capture type
|
||||
let cType = 'custom';
|
||||
let cValue = innerPattern;
|
||||
|
||||
for (const [type, p] of Object.entries(capturePatterns)) {
|
||||
if (innerPattern === p) {
|
||||
cType = type;
|
||||
cValue = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
newSegments.push({
|
||||
type: 'dynamic',
|
||||
captureType: cType,
|
||||
targetField: field,
|
||||
value: cType === 'custom' ? cValue : ''
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match a positional capture group: (pattern)
|
||||
const groupMatch = part.match(/^\((.+)\)$/);
|
||||
if (groupMatch) {
|
||||
const innerPattern = groupMatch[1];
|
||||
|
||||
let cType = 'custom';
|
||||
let cValue = innerPattern;
|
||||
|
||||
for (const [type, p] of Object.entries(capturePatterns)) {
|
||||
if (innerPattern === p) {
|
||||
cType = type;
|
||||
cValue = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
newSegments.push({
|
||||
type: 'dynamic',
|
||||
captureType: cType,
|
||||
targetField: undefined,
|
||||
value: cType === 'custom' ? cValue : ''
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, it's static (or a complex regex part we treat as static)
|
||||
// We unescape things that we know we escape
|
||||
let val = part.replace(/\\([.*+?^${}()|[\]\\])/g, '$1');
|
||||
newSegments.push({ type: 'static', value: val });
|
||||
}
|
||||
|
||||
if (newSegments.length > 0) {
|
||||
this.segments = newSegments;
|
||||
this.updateBuilder(); // Refresh generatedRegex and simulations
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to deconstruct regex:", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Regex Builder & Validation Methods ---
|
||||
|
||||
validateRawRegex() {
|
||||
this.rawRegexError = '';
|
||||
if (!this.current_regex.regex_pattern) {
|
||||
this.rawRegexError = 'Regex pattern is required.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new RegExp(this.current_regex.regex_pattern);
|
||||
if (!/\(.*\)/.test(this.current_regex.regex_pattern)) {
|
||||
this.rawRegexError = 'Must contain at least one capturing group ().';
|
||||
}
|
||||
} catch (e) {
|
||||
this.rawRegexError = 'Invalid regular expression syntax.';
|
||||
}
|
||||
}
|
||||
|
||||
onRawRegexChange() {
|
||||
this.validateRawRegex();
|
||||
if (this.inputMode === 'raw') {
|
||||
this.evaluateRegexAgainstSample();
|
||||
}
|
||||
}
|
||||
|
||||
setMode(mode: 'raw' | 'builder') {
|
||||
this.inputMode = mode;
|
||||
if (mode === 'builder') {
|
||||
this.updateBuilder();
|
||||
} else {
|
||||
if (this.generatedRegex && this.isTestValid) {
|
||||
this.current_regex.regex_pattern = this.generatedRegex;
|
||||
}
|
||||
this.validateRawRegex();
|
||||
this.evaluateRegexAgainstSample();
|
||||
}
|
||||
}
|
||||
|
||||
escapeRegex(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
updateBuilder() {
|
||||
let pattern = '';
|
||||
this.segments.forEach((seg, index) => {
|
||||
if (seg.type === 'static') {
|
||||
if (seg.value) {
|
||||
let escaped = this.escapeRegex(seg.value);
|
||||
pattern += escaped.trim();
|
||||
}
|
||||
} else {
|
||||
let capturePattern = '(.*)';
|
||||
if (seg.captureType === 'word') capturePattern = '(\\S+)';
|
||||
if (seg.captureType === 'ip') capturePattern = '(\\d{1,3}(?:\\.\\d{1,3}){3})';
|
||||
if (seg.captureType === 'mac') capturePattern = '([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})';
|
||||
if (seg.captureType === 'number') capturePattern = '(\\d+)';
|
||||
if (seg.captureType === 'custom' && seg.value) capturePattern = `(${seg.value})`;
|
||||
|
||||
if (seg.targetField) {
|
||||
pattern += `(?P<${seg.targetField}>${capturePattern.slice(1, -1)})`;
|
||||
} else {
|
||||
pattern += capturePattern;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < this.segments.length - 1) {
|
||||
pattern += '\\s*';
|
||||
}
|
||||
});
|
||||
|
||||
this.generatedRegex = pattern;
|
||||
this.evaluateRegexAgainstSample();
|
||||
}
|
||||
|
||||
evaluateRegexAgainstSample() {
|
||||
this.testFeedback = '';
|
||||
this.isTestValid = false;
|
||||
this.testResults = [];
|
||||
this.simulatedDbRow = null;
|
||||
|
||||
let regexToTest = this.inputMode === 'raw' ? this.current_regex.regex_pattern : this.generatedRegex;
|
||||
|
||||
if (!this.sampleLog || !regexToTest) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let jsPattern = regexToTest.replace(/\(\?P</g, '(?<');
|
||||
let re = new RegExp(jsPattern);
|
||||
let match = re.exec(this.sampleLog);
|
||||
|
||||
if (match) {
|
||||
this.isTestValid = true;
|
||||
this.testFeedback = 'Matches Found!';
|
||||
this.testResults.push({ field: 'Full Match', value: match[0] });
|
||||
|
||||
// Find the alert based on the select element matching
|
||||
const alertIdToMatch = this.current_regex.alert_id ? Number(this.current_regex.alert_id) : null;
|
||||
const selectedAlert = this.Alerts.find((a: any) => Number(a.id) === alertIdToMatch);
|
||||
|
||||
let detailValue = '(Inherited from Alert)';
|
||||
let levelValue = '(Inherited from Alert)';
|
||||
|
||||
if (this.current_regex.alert_enabled && selectedAlert) {
|
||||
detailValue = selectedAlert.name;
|
||||
levelValue = selectedAlert.level;
|
||||
} else if (!this.current_regex.alert_enabled) {
|
||||
detailValue = '(No Alert Linked)';
|
||||
levelValue = '(No Alert Linked)';
|
||||
}
|
||||
|
||||
let dbRow = {
|
||||
eventtype: this.current_regex.eventtype || '',
|
||||
src: 'custom regex',
|
||||
detail: detailValue,
|
||||
level: levelValue,
|
||||
status: this.current_regex.status !== undefined ? this.current_regex.status.toString() : '0',
|
||||
comment: this.sampleLog
|
||||
};
|
||||
|
||||
if (match.groups) {
|
||||
Object.keys(match.groups).forEach(key => {
|
||||
this.testResults.push({ field: `[Mapped: ${key}]`, value: match.groups![key] });
|
||||
if (key === 'comment') {
|
||||
(dbRow as any)[key] = match.groups![key];
|
||||
}
|
||||
});
|
||||
} else if (match.length > 1) {
|
||||
for (let i = 1; i < match.length; i++) {
|
||||
this.testResults.push({ field: `Capture ${i}`, value: match[i] });
|
||||
}
|
||||
}
|
||||
|
||||
this.simulatedDbRow = dbRow;
|
||||
} else {
|
||||
this.testFeedback = "Pattern doesn't match the sample log.";
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.testFeedback = 'Regex Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
getHighlightedSampleBoxHtml(): string {
|
||||
if (!this.sampleLog || !this.generatedRegex || !this.isTestValid) return this.sampleLog || '';
|
||||
try {
|
||||
let jsPattern = this.generatedRegex.replace(/\(\?P</g, '(?<');
|
||||
let re = new RegExp(`(${jsPattern})`, 'i');
|
||||
return this.sampleLog.replace(re, '<mark class="bg-warning text-dark">$1</mark>');
|
||||
} catch (e) {
|
||||
return this.sampleLog;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/app/views/syslog-regex/syslog-regex.module.ts
Normal file
60
src/app/views/syslog-regex/syslog-regex.module.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
ButtonGroupModule,
|
||||
ButtonModule,
|
||||
CardModule,
|
||||
FormModule,
|
||||
GridModule,
|
||||
ToastModule,
|
||||
ModalModule,
|
||||
BadgeModule,
|
||||
AccordionModule,
|
||||
NavModule,
|
||||
TabsModule,
|
||||
AlertModule,
|
||||
CollapseModule
|
||||
} from "@coreui/angular";
|
||||
import { HighlightJsModule } from 'ngx-highlight-js';
|
||||
import { SyslogRegexRoutingModule } from "./syslog-regex-routing.module";
|
||||
import { SyslogRegexComponent } from "./syslog-regex.component";
|
||||
import { NgxSuperSelectModule } from "ngx-super-select";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { MultiSelectModule } from 'primeng/multiselect';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SyslogRegexRoutingModule,
|
||||
CardModule,
|
||||
CommonModule,
|
||||
GridModule,
|
||||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
MultiSelectModule,
|
||||
TooltipModule,
|
||||
DropdownModule,
|
||||
ModalModule,
|
||||
ToastModule,
|
||||
FormsModule,
|
||||
BadgeModule,
|
||||
NgxSuperSelectModule,
|
||||
AccordionModule,
|
||||
NavModule,
|
||||
TabsModule,
|
||||
AlertModule,
|
||||
HighlightJsModule,
|
||||
CollapseModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [SyslogRegexComponent],
|
||||
})
|
||||
export class SyslogRegexModule { }
|
||||
|
|
@ -8,7 +8,7 @@ const routes: Routes = [
|
|||
path: '',
|
||||
component: SyslogComponent,
|
||||
data: {
|
||||
title: $localize`Mikrowizard System Logs`
|
||||
title: $localize`System Logs`
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="11" style="display: flex;flex-direction: column;align-items: flex-start;">
|
||||
<h5>Devices</h5>
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;" *ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by default. Use filters to modify the date and time.
|
||||
<h5>System Logs</h5>
|
||||
<c-alert color="warning" style="padding-top: 5px!important;font-size: 0.8rem;display: inline-block;"
|
||||
*ngIf="!filters['start_time'] && !filters['end_time']">
|
||||
<i class="fa-solid fa-triangle-exclamation mx-1"></i>Showing <strong>last 24 hours logs</strong> by
|
||||
default. Use filters to modify the date and time.
|
||||
</c-alert>
|
||||
</c-col>
|
||||
<c-col xs [lg]="1">
|
||||
|
|
@ -62,44 +64,144 @@
|
|||
</div>
|
||||
|
||||
</c-row>
|
||||
<gui-grid wid [rowDetail]="rowDetail" [horizontalGrid]="true" [rowHeight]="52" [source]="source"
|
||||
[columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[autoResizeWidth]="true">
|
||||
<gui-grid-column header="#No" type="NUMBER" field="index" width="1" align="CENTER">
|
||||
<ng-template let-value="item.index" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search logs..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gui-grid-column header="username" field="username">
|
||||
<ng-template let-value="item.username" let-item="item" let-index="index">
|
||||
<div class="gui-dev-info">
|
||||
<span class="gui-dev-info-name">{{ value }}</span>
|
||||
<span class="gui-dev-info-ip">{{ item.first_name }} {{ item.last_name }}</span>
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['username','section','action','ip']" selectionMode="single"
|
||||
(onRowSelect)="showLogDetails($event.data)">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="index" style="width: 5rem" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>#No</span>
|
||||
<p-sortIcon field="index"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="username" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Username</span>
|
||||
<span>
|
||||
<p-sortIcon field="username"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="section" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Section</span>
|
||||
<span>
|
||||
<p-sortIcon field="section"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="section" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="action" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Action</span>
|
||||
<span>
|
||||
<p-sortIcon field="action"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="action" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="ip" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>IP</span>
|
||||
<span>
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="created" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Time</span>
|
||||
<span>
|
||||
<p-sortIcon field="created"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="created" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr [pSelectableRow]="item" style="cursor: pointer;">
|
||||
<td>{{item.index}}</td>
|
||||
<td>
|
||||
<div class="gui-dev-info">
|
||||
<span class="gui-dev-info-name">{{ item.username }}</span>
|
||||
<span class="gui-dev-info-ip" style="font-size: 0.75rem; color: #666;">{{ item.first_name }} {{
|
||||
item.last_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{item.section}}</td>
|
||||
<td>{{item.action}}</td>
|
||||
<td>{{item.ip}}</td>
|
||||
<td>{{item.created}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="6">No logs found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<!-- Details Drawer -->
|
||||
<p-drawer [(visible)]="detailsVisible" position="right" [style]="{width: '450px'}" header="Log Details">
|
||||
<div *ngIf="selectedLog" class="p-3">
|
||||
<h5 class="border-bottom pb-2 mb-3"><i class="pi pi-info-circle me-2 text-primary"></i>System Log</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Section:</div>
|
||||
<div class="col-8">{{selectedLog.section}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Action:</div>
|
||||
<div class="col-8">{{selectedLog.action}}</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-4 fw-bold">Time:</div>
|
||||
<div class="col-8 text-muted small">{{selectedLog.created}}</div>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-user me-2 text-primary"></i>User Detail</h5>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">User:</div>
|
||||
<div class="col-8">{{selectedLog.username}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Name:</div>
|
||||
<div class="col-8">{{selectedLog.first_name}} {{selectedLog.last_name}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">IP:</div>
|
||||
<div class="col-8">{{selectedLog.ip}}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-4 fw-bold">Agent:</div>
|
||||
<div class="col-8 small text-wrap text-break" style="max-height: 80px; overflow-y: auto;">
|
||||
{{selectedLog.agent}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Section" field="section">
|
||||
<ng-template let-value="item.section" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="action" field="action">
|
||||
<ng-template let-value="item.action" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="ip" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Time" field="created">
|
||||
<ng-template let-value="item.created" let-item="item" let-index="index">
|
||||
{{ value }}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</div>
|
||||
|
||||
<h5 class="border-bottom pb-2 mb-3 mt-4"><i class="pi pi-code me-2 text-primary"></i>Raw Data</h5>
|
||||
<div class="bg-light p-3 rounded"
|
||||
style="max-height: 250px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85rem; border: 1px solid #dee2e6;">
|
||||
{{selectedLog.data}}
|
||||
</div>
|
||||
</div>
|
||||
</p-drawer>
|
||||
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,8 @@
|
|||
import { Component, OnInit, ViewEncapsulation } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild, ViewEncapsulation } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiRowDetail,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
|
||||
|
|
@ -22,10 +12,11 @@ import { formatInTimeZone } from "date-fns-tz";
|
|||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class SyslogComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid!: number;
|
||||
public uname!: string;
|
||||
public tz: string= "UTC";
|
||||
public filterText: string;
|
||||
public filterText!: string;
|
||||
public userid: number = 0;
|
||||
public filters: any = {
|
||||
start_time: false,
|
||||
end_time: false,
|
||||
|
|
@ -36,6 +27,10 @@ export class SyslogComponent implements OnInit {
|
|||
public event_section: any = [];
|
||||
public event_action: any = [];
|
||||
public filters_visible: boolean = false;
|
||||
public detailsVisible: boolean = false;
|
||||
public selectedLog: any = null;
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
private router: Router,
|
||||
|
|
@ -66,95 +61,18 @@ export class SyslogComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public Selectedrows: any;
|
||||
public userid: number = 0;
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
public campaignOnestart: any;
|
||||
public campaignOneend: any;
|
||||
rowDetail: GuiRowDetail = {
|
||||
enabled: true,
|
||||
template: (item) => {
|
||||
return `
|
||||
<div class='log-detail' style="width: 355px;color:#fff;background-color:#3399ff">
|
||||
<h2>System Log :</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Section</td>
|
||||
<td>${item.section}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Action</td>
|
||||
<td>${item.action}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td>${item.created}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2 style="margin-top: 5px;">User Detail :
|
||||
</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>User</td>
|
||||
<td>${item.username}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FirstName</td>
|
||||
<td>${item.first_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LastName</td>
|
||||
<td>${item.last_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>${item.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agent</td>
|
||||
<td><div style="height: 40px;overflow-y: scroll;">${item.agent}</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="code-title">data</div>
|
||||
<code>
|
||||
${item.data}
|
||||
</code>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
showLogDetails(log: any) {
|
||||
this.selectedLog = log;
|
||||
this.detailsVisible = true;
|
||||
}
|
||||
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
// Removed legacy GuiGrid configs
|
||||
ngOnInit(): void {
|
||||
var _self = this;
|
||||
this.userid = Number(this.route.snapshot.paramMap.get("userid"));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import {
|
|||
import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
|
||||
import { SyslogRoutingModule } from "./syslog-routing.module";
|
||||
import { SyslogComponent } from "./syslog.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { DrawerModule } from 'primeng/drawer';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { MatDatepickerModule } from "@angular/material/datepicker";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
|
|
@ -28,7 +30,9 @@ import { MatSelectModule } from "@angular/material/select";
|
|||
GridModule,
|
||||
FormsModule,
|
||||
ButtonModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
DrawerModule,
|
||||
InputTextModule,
|
||||
CollapseModule,
|
||||
DropdownModule,
|
||||
MatInputModule,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="10">
|
||||
Users
|
||||
User Managment
|
||||
</c-col>
|
||||
<c-col xs [lg]="2" style="text-align: right;">
|
||||
<button cButton color="primary" (click)="editAddUser({},'showadd')"><i
|
||||
|
|
@ -12,41 +12,91 @@
|
|||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body >
|
||||
<gui-grid [autoResizeWidth]="true" [source]="source" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[autoResizeWidth]=true [paging]="paging">
|
||||
<gui-grid-column header="User Name" field="username">
|
||||
<ng-template let-value="item.username" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="First Name" field="first_name">
|
||||
<ng-template let-value="item.first_name" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Last Name" field="last_name">
|
||||
<ng-template let-value="item.last_name" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Role" field="role">
|
||||
<ng-template let-value="item.role" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="warning" size="sm" (click)="editAddUser(item,'edit');" ><i
|
||||
class="fa-regular fa-pen-to-square"></i></button>
|
||||
<button cButton color="danger" size="sm" class="mx-1" (click)="confirm_delete(item);"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
<c-card-body>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search users..." (input)="applyFilterGlobal($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button *ngIf="ispro" cButton color="secondary" size="sm" (click)="showrest(item);">
|
||||
<i class="fa-solid fa-fingerprint"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['username', 'first_name', 'last_name', 'role']" [loading]="loading">
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="username" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>User Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="username"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="first_name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>First Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="first_name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="first_name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="last_name" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Last Name</span>
|
||||
<span>
|
||||
<p-sortIcon field="last_name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="last_name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="role" pResizableColumn>
|
||||
<div class="justify-between">
|
||||
<span>Role</span>
|
||||
<span>
|
||||
<p-sortIcon field="role"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="role" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 120px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>{{item.username}}</td>
|
||||
<td>{{item.first_name}}</td>
|
||||
<td>{{item.last_name}}</td>
|
||||
<td>{{item.role}}</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
<button cButton color="warning" size="sm" (click)="editAddUser(item,'edit');" pTooltip="Edit User">
|
||||
<i class="fa-regular fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item);" pTooltip="Delete User">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
<button *ngIf="ispro" cButton color="secondary" size="sm" (click)="showrest(item);"
|
||||
pTooltip="Security Restrictions">
|
||||
<i class="fa-solid fa-fingerprint"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center p-4">No users found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
|
@ -54,285 +104,304 @@
|
|||
<c-modal-header>
|
||||
|
||||
|
||||
<c-modal #EditTaskModal backdrop="static" size="lg" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
|
||||
<c-modal-header>
|
||||
<h5 *ngIf="SelectedUser['action']=='edit'" cModalTitle><i class="fa-solid fa-user-pen me-1"></i>Edit: {{SelectedUser['username']}}</h5>
|
||||
<h5 *ngIf="SelectedUser['action']=='add'" cModalTitle><i class="fa-solid fa-user-plus me-1"></i>Add User</h5>
|
||||
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-3">
|
||||
<!-- User Details -->
|
||||
<div class="user-form-section mb-4">
|
||||
<div class="section-header mb-3">
|
||||
<h6 class="section-title mb-1"><i class="fa-solid fa-user me-2"></i>Basic Information</h6>
|
||||
<small class="text-muted">Enter the user's personal details and login credentials</small>
|
||||
</div>
|
||||
<c-row class="g-3">
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Username (required for login)" [(ngModel)]="SelectedUser['username']"
|
||||
class="form-input" title="Unique username for system login" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Email Address (for notifications)" [(ngModel)]="SelectedUser['email']"
|
||||
class="form-input" type="email" title="Valid email address for system notifications" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="4">
|
||||
<input cFormControl placeholder="First Name (optional)" [(ngModel)]="SelectedUser['first_name']"
|
||||
class="form-input" title="User's first name" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="4">
|
||||
<input cFormControl placeholder="Last Name (optional)" [(ngModel)]="SelectedUser['last_name']"
|
||||
class="form-input" title="User's last name" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="4">
|
||||
<input type="password" cFormControl placeholder="Password (min 6 characters)" [(ngModel)]="SelectedUser['password']"
|
||||
class="form-input" title="Secure password for user login" />
|
||||
</c-col>
|
||||
</c-row>
|
||||
</div>
|
||||
|
||||
<!-- System Permissions -->
|
||||
<div class="permissions-section mb-4">
|
||||
<div class="section-header mb-3">
|
||||
<h6 class="section-title mb-1"><i class="fa-solid fa-shield-halved me-2"></i>System Access Levels</h6>
|
||||
<small class="text-muted">Control what system features this user can access and modify</small>
|
||||
</div>
|
||||
<div class="permission-legend mb-2">
|
||||
<span class="legend-item"><span class="legend-color bg-info"></span>R = Read Only</span>
|
||||
<span class="legend-item"><span class="legend-color bg-warning"></span>W = Read & Write</span>
|
||||
<span class="legend-item"><span class="legend-color bg-success"></span>F = Full Control</span>
|
||||
<span class="legend-item"><span class="legend-color bg-secondary"></span>× = No Access</span>
|
||||
</div>
|
||||
<div class="permissions-compact">
|
||||
<div *ngFor='let perm of adminperms | keyvalue' class="perm-row">
|
||||
<span class="perm-label">{{ perm.key.replace('_', ' ') }}</span>
|
||||
<div class="perm-controls">
|
||||
<button cButton size="sm" variant="outline" color="info" [active]="adminperms[perm.key]=='read'"
|
||||
(click)="setRadioValue(perm.key,'read')" title="Read Only Access">R</button>
|
||||
<button cButton size="sm" variant="outline" color="warning" [active]="adminperms[perm.key]=='write'"
|
||||
(click)="setRadioValue(perm.key,'write')" title="Read & Write Access">W</button>
|
||||
<button cButton size="sm" variant="outline" color="success" [active]="adminperms[perm.key]=='full'"
|
||||
(click)="setRadioValue(perm.key,'full')" title="Full Control Access">F</button>
|
||||
<button cButton size="sm" variant="outline" color="secondary" [active]="adminperms[perm.key]=='none'"
|
||||
(click)="setRadioValue(perm.key,'none')" title="No Access">×</button>
|
||||
</div>
|
||||
<c-modal #EditTaskModal backdrop="static" size="lg" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
|
||||
<c-modal-header>
|
||||
<h5 *ngIf="SelectedUser['action']=='edit'" cModalTitle><i class="fa-solid fa-user-pen me-1"></i>Edit:
|
||||
{{SelectedUser['username']}}</h5>
|
||||
<h5 *ngIf="SelectedUser['action']=='add'" cModalTitle><i class="fa-solid fa-user-plus me-1"></i>Add User</h5>
|
||||
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body class="p-3">
|
||||
<!-- User Details -->
|
||||
<div class="user-form-section mb-4">
|
||||
<div class="section-header mb-3">
|
||||
<h6 class="section-title mb-1"><i class="fa-solid fa-user me-2"></i>Basic Information</h6>
|
||||
<small class="text-muted">Enter the user's personal details and login credentials</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Permissions -->
|
||||
<div class="device-permissions-section">
|
||||
<div class="section-header mb-3">
|
||||
<h6 class="section-title mb-1"><i class="fa-solid fa-network-wired me-2"></i>Device Access Control</h6>
|
||||
<small class="text-muted">Grant access to specific device groups with defined permission levels</small>
|
||||
</div>
|
||||
|
||||
<!-- Add New Permission -->
|
||||
<div class="add-permission-card mb-3">
|
||||
<div class="add-header">
|
||||
<i class="fa-solid fa-plus-circle text-primary me-2"></i>
|
||||
<span class="fw-semibold">Add Device Permission</span>
|
||||
</div>
|
||||
<c-row class="g-2 mt-2">
|
||||
<c-col xs="12" md="5">
|
||||
<div class="search-select-wrapper">
|
||||
<label class="search-label">Device Group</label>
|
||||
<input cFormControl
|
||||
[(ngModel)]="devgroupSearch"
|
||||
(input)="filterDevGroups($event)"
|
||||
(focus)="showDevGroupDropdown = true"
|
||||
(blur)="hideDevGroupDropdown()"
|
||||
placeholder="Search and select device group..."
|
||||
class="search-input compact-select"
|
||||
autocomplete="off" />
|
||||
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length > 0" class="search-dropdown">
|
||||
<div *ngFor="let group of filteredDevGroups"
|
||||
class="search-option"
|
||||
(mousedown)="selectDevGroup(group)">
|
||||
{{group.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length === 0 && devgroupSearch" class="search-no-results">
|
||||
No groups found
|
||||
</div>
|
||||
</div>
|
||||
<c-row class="g-3">
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Username (required for login)" [(ngModel)]="SelectedUser['username']"
|
||||
class="form-input" title="Unique username for system login" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="5">
|
||||
<div class="search-select-wrapper">
|
||||
<label class="search-label">Permission Level</label>
|
||||
<input cFormControl
|
||||
[(ngModel)]="permissionSearch"
|
||||
(input)="filterPermissions($event)"
|
||||
(focus)="showPermissionDropdown = true"
|
||||
(blur)="hidePermissionDropdown()"
|
||||
placeholder="Search and select permission..."
|
||||
class="search-input compact-select"
|
||||
autocomplete="off" />
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
|
||||
<div *ngFor="let perm of filteredPermissions"
|
||||
class="search-option"
|
||||
(mousedown)="selectPermission(perm)">
|
||||
{{perm.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch" class="search-no-results">
|
||||
No permissions found
|
||||
</div>
|
||||
</div>
|
||||
<c-col xs="12" md="6">
|
||||
<input cFormControl placeholder="Email Address (for notifications)" [(ngModel)]="SelectedUser['email']"
|
||||
class="form-input" type="email" title="Valid email address for system notifications" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="2" class="d-flex align-items-end">
|
||||
<button *ngIf="SelectedUser['action']=='edit'" cButton color="success" size="sm" class="w-100 add-btn"
|
||||
(click)="add_user_perm()" [disabled]="!devgroup || !permission">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Access
|
||||
</button>
|
||||
<button *ngIf="SelectedUser['action']=='add'" cButton color="success" size="sm" class="w-100 add-btn"
|
||||
(click)="add_new_user_perm()" [disabled]="!devgroup || !permission">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Access
|
||||
</button>
|
||||
<c-col xs="12" md="4">
|
||||
<input cFormControl placeholder="First Name (optional)" [(ngModel)]="SelectedUser['first_name']"
|
||||
class="form-input" title="User's first name" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="4">
|
||||
<input cFormControl placeholder="Last Name (optional)" [(ngModel)]="SelectedUser['last_name']"
|
||||
class="form-input" title="User's last name" />
|
||||
</c-col>
|
||||
<c-col xs="12" md="4">
|
||||
<input type="password" cFormControl placeholder="Password (min 6 characters)"
|
||||
[(ngModel)]="SelectedUser['password']" class="form-input" title="Secure password for user login" />
|
||||
</c-col>
|
||||
</c-row>
|
||||
</div>
|
||||
|
||||
<!-- Current Permissions -->
|
||||
<div class="current-permissions">
|
||||
<div class="permissions-header">
|
||||
<h6 class="mb-1"><i class="fa-solid fa-list-check me-2 text-success"></i>Current Device Access</h6>
|
||||
<span class="badge bg-info">{{userperms.length}} permission(s)</span>
|
||||
<!-- System Permissions -->
|
||||
<div class="permissions-section mb-4">
|
||||
<div class="section-header mb-3">
|
||||
<h6 class="section-title mb-1"><i class="fa-solid fa-shield-halved me-2"></i>System Access Levels</h6>
|
||||
<small class="text-muted">Control what system features this user can access and modify</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="userperms.length>0" class="permissions-list mt-2">
|
||||
<div *ngFor="let perm of userperms; let i = index" class="permission-item">
|
||||
<div class="perm-number">{{i + 1}}</div>
|
||||
<div class="perm-info">
|
||||
<div class="perm-main">
|
||||
<i class="fa-solid fa-server me-1 text-primary"></i>
|
||||
<span class="group-name">{{perm.group_name}}</span>
|
||||
</div>
|
||||
<div class="perm-detail">
|
||||
<i class="fa-solid fa-key me-1 text-warning"></i>
|
||||
<span class="perm-name">{{perm.perm_name}}</span>
|
||||
</div>
|
||||
<div class="permission-legend mb-2">
|
||||
<span class="legend-item"><span class="legend-color bg-info"></span>R = Read Only</span>
|
||||
<span class="legend-item"><span class="legend-color bg-warning"></span>W = Read & Write</span>
|
||||
<span class="legend-item"><span class="legend-color bg-success"></span>F = Full Control</span>
|
||||
<span class="legend-item"><span class="legend-color bg-secondary"></span>× = No Access</span>
|
||||
</div>
|
||||
<div class="permissions-compact">
|
||||
<div *ngFor='let perm of adminperms | keyvalue' class="perm-row">
|
||||
<span class="perm-label">{{ perm.key.replace('_', ' ') }}</span>
|
||||
<div class="perm-controls">
|
||||
<button cButton size="sm" variant="outline" color="info" [active]="adminperms[perm.key]=='read'"
|
||||
(click)="setRadioValue(perm.key,'read')" title="Read Only Access">R</button>
|
||||
<button cButton size="sm" variant="outline" color="warning" [active]="adminperms[perm.key]=='write'"
|
||||
(click)="setRadioValue(perm.key,'write')" title="Read & Write Access">W</button>
|
||||
<button cButton size="sm" variant="outline" color="success" [active]="adminperms[perm.key]=='full'"
|
||||
(click)="setRadioValue(perm.key,'full')" title="Full Control Access">F</button>
|
||||
<button cButton size="sm" variant="outline" color="secondary" [active]="adminperms[perm.key]=='none'"
|
||||
(click)="setRadioValue(perm.key,'none')" title="No Access">×</button>
|
||||
</div>
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="confirm_delete_perm(perm)"
|
||||
title="Remove this permission" class="remove-btn">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="userperms.length==0" class="empty-permissions">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-shield-xmark text-muted"></i>
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
<strong>No device access granted</strong>
|
||||
<p class="mb-0 text-muted">Add permissions above to grant access to device groups</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer class="d-flex justify-content-between flex-wrap gap-2 p-2">
|
||||
<div>
|
||||
<button *ngIf="SelectedUser['role']!='disabled'" (click)="SelectedUser['role']='disabled'" cButton color="danger" size="sm">
|
||||
<i class="fa-solid fa-user-slash me-1"></i><span class="d-none d-sm-inline">Deactivate</span>
|
||||
</button>
|
||||
<button *ngIf="SelectedUser['role']=='disabled'" (click)="SelectedUser['role']='admin'" cButton color="success" size="sm">
|
||||
<i class="fa-solid fa-user-check me-1"></i><span class="d-none d-sm-inline">Activate</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button *ngIf="SelectedUser['action']=='add'" (click)="submit('add')" cButton color="primary" size="sm">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add
|
||||
</button>
|
||||
<button *ngIf="SelectedUser['action']=='edit'" (click)="submit('edit')" cButton color="primary" size="sm">
|
||||
<i class="fa-solid fa-save me-1"></i>Save
|
||||
</button>
|
||||
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" size="sm">
|
||||
<i class="fa-solid fa-times me-1"></i>Close
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-modal #RestrictionsTaskModal *ngIf="ispro && userresttrictions" backdrop="static" size="lg" [(visible)]="RestrictionsTaskModalVisible" id="RestrictionsTaskModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Security Restrictions of {{SelectedUser['username']}}</h5>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td><h6>TOTP status :</h6></td>
|
||||
<td>
|
||||
<c-form-check sizing="xl" switch>
|
||||
<input cFormCheckInput [(ngModel)]="userresttrictions['totp']" [checked]="userresttrictions['totp']" type="checkbox" />
|
||||
<label *ngIf="userresttrictions['totp']" cFormCheckLabel> TOTP is active</label>
|
||||
<label *ngIf="!userresttrictions['totp']" cFormCheckLabel> TOTP is deactive</label>
|
||||
</c-form-check>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h6>Use OTP for device login:</h6></td>
|
||||
<td>
|
||||
<c-button-group aria-label="Basic example" role="group">
|
||||
<button cButton color="info" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='system'"
|
||||
(click)="userresttrictions['device-totp']='system'">System Defined</button>
|
||||
<button cButton color="danger" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='yes'"
|
||||
(click)="userresttrictions['device-totp']='yes'">TOTP</button>
|
||||
<button cButton color="success" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='no'"
|
||||
(click)="userresttrictions['device-totp']='no'">Password</button>
|
||||
</c-button-group>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h6>Restrict IP access:</h6></td>
|
||||
<td>
|
||||
<c-form-check sizing="xl" switch>
|
||||
<input cFormCheckInput [(ngModel)]="userresttrictions['ip']" [checked]="userresttrictions['ip']" type="checkbox" />
|
||||
<label *ngIf="userresttrictions['ip']" cFormCheckLabel> Restricted</label>
|
||||
<label *ngIf="!userresttrictions['ip']" cFormCheckLabel> Not Restricted</label>
|
||||
</c-form-check>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<c-input-group *ngIf="userresttrictions['ip'] && userresttrictions['allowed_ips'].length>0" class="mb-3">
|
||||
<h5>Allowed ips :</h5>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="userresttrictions['allowed_ips']" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[autoResizeWidth]=true [paging]="paging" >
|
||||
<gui-grid-column header="IP Address" >
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
{{item}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Action" width="80" align="center">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
<button cButton color="danger" (click)="delete_ip(item)"><i class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-input-group>
|
||||
<hr />
|
||||
<table *ngIf="userresttrictions['ip']" class="mb-3">
|
||||
<td style="width: 30%;">
|
||||
<span>Add new IP</span>
|
||||
</td>
|
||||
<td>
|
||||
<div >
|
||||
<input cFormControl id="floatingInput" placeholder="IP address/cidr" [(ngModel)]="ipaddress" />
|
||||
<!-- Device Permissions -->
|
||||
<div class="device-permissions-section">
|
||||
<div class="section-header mb-3">
|
||||
<h6 class="section-title mb-1"><i class="fa-solid fa-network-wired me-2"></i>Device Access Control</h6>
|
||||
<small class="text-muted">Grant access to specific device groups with defined permission levels</small>
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<button cButton color="primary" (click)="add_ip()">Add+</button>
|
||||
</td>
|
||||
</table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="save_sec()" cButton color="primary">Save</button>
|
||||
<button [cModalToggle]="RestrictionsTaskModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Add New Permission -->
|
||||
<div class="add-permission-card mb-3">
|
||||
<div class="add-header">
|
||||
<i class="fa-solid fa-plus-circle text-primary me-2"></i>
|
||||
<span class="fw-semibold">Add Device Permission</span>
|
||||
</div>
|
||||
<c-row class="g-2 mt-2">
|
||||
<c-col xs="12" md="5">
|
||||
<div class="search-select-wrapper">
|
||||
<label class="search-label">Device Group</label>
|
||||
<input cFormControl [(ngModel)]="devgroupSearch" (input)="filterDevGroups($event)"
|
||||
(focus)="showDevGroupDropdown = true" (blur)="hideDevGroupDropdown()"
|
||||
placeholder="Search and select device group..." class="search-input compact-select"
|
||||
autocomplete="off" />
|
||||
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length > 0" class="search-dropdown">
|
||||
<div *ngFor="let group of filteredDevGroups" class="search-option"
|
||||
(mousedown)="selectDevGroup(group)">
|
||||
{{group.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showDevGroupDropdown && filteredDevGroups.length === 0 && devgroupSearch"
|
||||
class="search-no-results">
|
||||
No groups found
|
||||
</div>
|
||||
</div>
|
||||
</c-col>
|
||||
<c-col xs="12" md="5">
|
||||
<div class="search-select-wrapper">
|
||||
<label class="search-label">Permission Level</label>
|
||||
<input cFormControl [(ngModel)]="permissionSearch" (input)="filterPermissions($event)"
|
||||
(focus)="showPermissionDropdown = true" (blur)="hidePermissionDropdown()"
|
||||
placeholder="Search and select permission..." class="search-input compact-select"
|
||||
autocomplete="off" />
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
|
||||
<div *ngFor="let perm of filteredPermissions" class="search-option"
|
||||
(mousedown)="selectPermission(perm)">
|
||||
{{perm.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch"
|
||||
class="search-no-results">
|
||||
No permissions found
|
||||
</div>
|
||||
</div>
|
||||
</c-col>
|
||||
<c-col xs="12" md="2" class="d-flex align-items-end">
|
||||
<button *ngIf="SelectedUser['action']=='edit'" cButton color="success" size="sm" class="w-100 add-btn"
|
||||
(click)="add_user_perm()" [disabled]="!devgroup || !permission">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Access
|
||||
</button>
|
||||
<button *ngIf="SelectedUser['action']=='add'" cButton color="success" size="sm" class="w-100 add-btn"
|
||||
(click)="add_new_user_perm()" [disabled]="!devgroup || !permission">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add Access
|
||||
</button>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</div>
|
||||
|
||||
<!-- Current Permissions -->
|
||||
<div class="current-permissions">
|
||||
<div class="permissions-header">
|
||||
<h6 class="mb-1"><i class="fa-solid fa-list-check me-2 text-success"></i>Current Device Access</h6>
|
||||
<span class="badge bg-info">{{userperms.length}} permission(s)</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="userperms.length>0" class="permissions-list mt-2">
|
||||
<div *ngFor="let perm of userperms; let i = index" class="permission-item">
|
||||
<div class="perm-number">{{i + 1}}</div>
|
||||
<div class="perm-info">
|
||||
<div class="perm-main">
|
||||
<i class="fa-solid fa-server me-1 text-primary"></i>
|
||||
<span class="group-name">{{perm.group_name}}</span>
|
||||
</div>
|
||||
<div class="perm-detail">
|
||||
<i class="fa-solid fa-key me-1 text-warning"></i>
|
||||
<span class="perm-name">{{perm.perm_name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="confirm_delete_perm(perm)"
|
||||
title="Remove this permission" class="remove-btn">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="userperms.length==0" class="empty-permissions">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-shield-xmark text-muted"></i>
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
<strong>No device access granted</strong>
|
||||
<p class="mb-0 text-muted">Add permissions above to grant access to device groups</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer class="d-flex justify-content-between flex-wrap gap-2 p-2">
|
||||
<div>
|
||||
<button *ngIf="SelectedUser['role']!='disabled'" (click)="SelectedUser['role']='disabled'" cButton
|
||||
color="danger" size="sm">
|
||||
<i class="fa-solid fa-user-slash me-1"></i><span class="d-none d-sm-inline">Deactivate</span>
|
||||
</button>
|
||||
<button *ngIf="SelectedUser['role']=='disabled'" (click)="SelectedUser['role']='admin'" cButton color="success"
|
||||
size="sm">
|
||||
<i class="fa-solid fa-user-check me-1"></i><span class="d-none d-sm-inline">Activate</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button *ngIf="SelectedUser['action']=='add'" (click)="submit('add')" cButton color="primary" size="sm">
|
||||
<i class="fa-solid fa-plus me-1"></i>Add
|
||||
</button>
|
||||
<button *ngIf="SelectedUser['action']=='edit'" (click)="submit('edit')" cButton color="primary" size="sm">
|
||||
<i class="fa-solid fa-save me-1"></i>Save
|
||||
</button>
|
||||
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" size="sm">
|
||||
<i class="fa-solid fa-times me-1"></i>Close
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-modal #RestrictionsTaskModal *ngIf="ispro && userresttrictions" backdrop="static" size="lg"
|
||||
[(visible)]="RestrictionsTaskModalVisible" id="RestrictionsTaskModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Security Restrictions of {{SelectedUser['username']}}</h5>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<h6>TOTP status :</h6>
|
||||
</td>
|
||||
<td>
|
||||
<c-form-check sizing="xl" switch>
|
||||
<input cFormCheckInput [(ngModel)]="userresttrictions['totp']" [checked]="userresttrictions['totp']"
|
||||
type="checkbox" />
|
||||
<label *ngIf="userresttrictions['totp']" cFormCheckLabel> TOTP is active</label>
|
||||
<label *ngIf="!userresttrictions['totp']" cFormCheckLabel> TOTP is deactive</label>
|
||||
</c-form-check>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Use OTP for device login:</h6>
|
||||
</td>
|
||||
<td>
|
||||
<c-button-group aria-label="Basic example" role="group">
|
||||
<button cButton color="info" variant="outline" size="sm"
|
||||
[active]="userresttrictions['device-totp']=='system'"
|
||||
(click)="userresttrictions['device-totp']='system'">System Defined</button>
|
||||
<button cButton color="danger" variant="outline" size="sm"
|
||||
[active]="userresttrictions['device-totp']=='yes'"
|
||||
(click)="userresttrictions['device-totp']='yes'">TOTP</button>
|
||||
<button cButton color="success" variant="outline" size="sm"
|
||||
[active]="userresttrictions['device-totp']=='no'"
|
||||
(click)="userresttrictions['device-totp']='no'">Password</button>
|
||||
</c-button-group>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Restrict IP access:</h6>
|
||||
</td>
|
||||
<td>
|
||||
<c-form-check sizing="xl" switch>
|
||||
<input cFormCheckInput [(ngModel)]="userresttrictions['ip']" [checked]="userresttrictions['ip']"
|
||||
type="checkbox" />
|
||||
<label *ngIf="userresttrictions['ip']" cFormCheckLabel> Restricted</label>
|
||||
<label *ngIf="!userresttrictions['ip']" cFormCheckLabel> Not Restricted</label>
|
||||
</c-form-check>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<c-input-group *ngIf="userresttrictions['ip'] && userresttrictions['allowed_ips'].length>0" class="mb-3">
|
||||
<h5>Allowed ips :</h5>
|
||||
<p-table [value]="userresttrictions['allowed_ips']" [paginator]="true" [rows]="5" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm" class="w-100">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th style="width: 80px" class="text-center">Action</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>{{item}}</td>
|
||||
<td class="text-center">
|
||||
<button cButton color="danger" size="sm" (click)="delete_ip(item)" pTooltip="Remove IP">
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="2" class="text-center p-2">No restricted IPs.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-input-group>
|
||||
<hr />
|
||||
<table *ngIf="userresttrictions['ip']" class="mb-3">
|
||||
<td style="width: 30%;">
|
||||
<span>Add new IP</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<input cFormControl id="floatingInput" placeholder="IP address/cidr" [(ngModel)]="ipaddress" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<button cButton color="primary" (click)="add_ip()">Add+</button>
|
||||
</td>
|
||||
</table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="save_sec()" cButton color="primary">Save</button>
|
||||
<button [cModalToggle]="RestrictionsTaskModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
|
||||
|
||||
|
|
@ -376,7 +445,8 @@
|
|||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-modal #DeletePermConfirmModal backdrop="static" [(visible)]="DeletePermConfirmModalVisible" id="DeletePermConfirmModal">
|
||||
<c-modal #DeletePermConfirmModal backdrop="static" [(visible)]="DeletePermConfirmModalVisible"
|
||||
id="DeletePermConfirmModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Confirm delete {{ SelectedUser['name'] }}</h5>
|
||||
<button [cModalToggle]="DeletePermConfirmModal.id" cButtonClose></button>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
|
||||
import { Component, OnInit, QueryList, ViewChildren, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiGridComponent,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
|
|
@ -21,11 +12,11 @@ import { ToasterComponent } from "@coreui/angular";
|
|||
styleUrls: ["user_manager.scss"],
|
||||
})
|
||||
export class UserManagerComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public ispro:boolean=false;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public ispro: boolean = false;
|
||||
|
||||
gridComponent: GuiGridComponent;
|
||||
@ViewChild('dt') table!: Table;
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
delay: 10000,
|
||||
|
|
@ -64,7 +55,6 @@ export class UserManagerComponent implements OnInit {
|
|||
}
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = false;
|
||||
public rows: any = [];
|
||||
public SelectedUser: any = {};
|
||||
|
|
@ -87,8 +77,8 @@ export class UserManagerComponent implements OnInit {
|
|||
public DeletePermConfirmModalVisible: boolean = false;
|
||||
public userperms: any = {};
|
||||
public userresttrictions: any = false;
|
||||
public ipaddress:string="";
|
||||
public adminperms: { [index: string]: string };
|
||||
public ipaddress: string = "";
|
||||
public adminperms: { [index: string]: string } = {};
|
||||
public defadminperms: { [index: string]: string } = {
|
||||
device: "none",
|
||||
device_group: "none",
|
||||
|
|
@ -103,11 +93,6 @@ export class UserManagerComponent implements OnInit {
|
|||
system_backup: "none",
|
||||
};
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
|
||||
options: Partial<NgxSuperSelectOptions> = {
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
|
|
@ -117,34 +102,20 @@ export class UserManagerComponent implements OnInit {
|
|||
enableDarkMode: false,
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
setRadioValue(key: string, value: string): void {
|
||||
this.adminperms[key] = value;
|
||||
}
|
||||
|
||||
filterDevGroups(event: any): void {
|
||||
const query = event.target.value.toLowerCase();
|
||||
this.filteredDevGroups = this.allDevGroups.filter((group: any) =>
|
||||
this.filteredDevGroups = this.allDevGroups.filter((group: any) =>
|
||||
group.name.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
filterPermissions(event: any): void {
|
||||
const query = event.target.value.toLowerCase();
|
||||
this.filteredPermissions = this.allPerms.filter((perm: any) =>
|
||||
this.filteredPermissions = this.allPerms.filter((perm: any) =>
|
||||
perm.name.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
|
@ -169,11 +140,9 @@ export class UserManagerComponent implements OnInit {
|
|||
setTimeout(() => this.showPermissionDropdown = false, 200);
|
||||
}
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGridTable();
|
||||
|
|
@ -188,12 +157,12 @@ export class UserManagerComponent implements OnInit {
|
|||
);
|
||||
componentRef.instance["closeButton"] = props.closeButton;
|
||||
}
|
||||
totp(item:any){
|
||||
totp(item: any) {
|
||||
this.SelectedUser = item;
|
||||
this.data_provider.totp('enable',this.SelectedUser.id).then((res) => {
|
||||
if(res.status == "success"){
|
||||
this.data_provider.totp('enable', this.SelectedUser.id).then((res) => {
|
||||
if (res.status == "success") {
|
||||
this.show_toast("Success", "Totp generated successfully", "success");
|
||||
}else{
|
||||
} else {
|
||||
this.show_toast("Error", res.err, "danger");
|
||||
}
|
||||
});
|
||||
|
|
@ -218,15 +187,15 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
if ("id" in res && !("status" in res)) {
|
||||
_self.initGridTable();
|
||||
this.EditTaskModalVisible = false;
|
||||
} else {
|
||||
//show error
|
||||
_self.show_toast("Error", res.err, "danger");
|
||||
else {
|
||||
if ("id" in res && !("status" in res)) {
|
||||
_self.initGridTable();
|
||||
this.EditTaskModalVisible = false;
|
||||
} else {
|
||||
//show error
|
||||
_self.show_toast("Error", res.err, "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (_self.userperms.length > 0) {
|
||||
|
|
@ -243,9 +212,9 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.initGridTable();
|
||||
_self.EditTaskModalVisible = false;
|
||||
else {
|
||||
_self.initGridTable();
|
||||
_self.EditTaskModalVisible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -262,27 +231,30 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.allPerms = res.map((x: any) => {
|
||||
return { id: x["id"], name: x.name };
|
||||
});
|
||||
_self.filteredPermissions = [..._self.allPerms];
|
||||
_self.data_provider.get_devgroup_list().then((res) => {
|
||||
if ("error" in res && res.error.indexOf("Unauthorized")) {
|
||||
_self.show_toast(
|
||||
"Error",
|
||||
"You are not authorized to perform this action",
|
||||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.allDevGroups = res.map((x: any) => {
|
||||
else if ("err" in res) {
|
||||
_self.show_toast("Error", res.err, "danger");
|
||||
}
|
||||
else {
|
||||
_self.allPerms = res.map((x: any) => {
|
||||
return { id: x["id"], name: x.name };
|
||||
});
|
||||
_self.filteredDevGroups = [..._self.allDevGroups];
|
||||
_self.filteredPermissions = [..._self.allPerms];
|
||||
_self.data_provider.get_devgroup_list().then((res) => {
|
||||
if ("error" in res && res.error.indexOf("Unauthorized")) {
|
||||
_self.show_toast(
|
||||
"Error",
|
||||
"You are not authorized to perform this action",
|
||||
"danger"
|
||||
);
|
||||
}
|
||||
else {
|
||||
_self.allDevGroups = res.map((x: any) => {
|
||||
return { id: x["id"], name: x.name };
|
||||
});
|
||||
_self.filteredDevGroups = [..._self.allDevGroups];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (action == "showadd") {
|
||||
this.userperms = [];
|
||||
|
|
@ -303,6 +275,10 @@ export class UserManagerComponent implements OnInit {
|
|||
this.EditTaskModalVisible = true;
|
||||
return;
|
||||
}
|
||||
if (item.username == "system") {
|
||||
this.show_toast("Error", "System user cannot be edited", "danger");
|
||||
return;
|
||||
}
|
||||
this.SelectedUser = { ...item };
|
||||
if (this.SelectedUser["adminperms"].length > 0) {
|
||||
this.adminperms = JSON.parse(this.SelectedUser["adminperms"]);
|
||||
|
|
@ -316,13 +292,13 @@ export class UserManagerComponent implements OnInit {
|
|||
_self.EditTaskModalVisible = true;
|
||||
}
|
||||
|
||||
checkIpAddress(ip:string) {
|
||||
checkIpAddress(ip: string) {
|
||||
const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|\/|)){4}\b(0?[1-9]|1[0-9]|2[0-9]|3[0-2])\b$/;
|
||||
return ipv4Pattern.test(ip)
|
||||
}
|
||||
|
||||
showrest(item: any) {
|
||||
var _self=this;
|
||||
var _self = this;
|
||||
this.SelectedUser = { ...item };
|
||||
|
||||
this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => {
|
||||
|
|
@ -330,33 +306,33 @@ export class UserManagerComponent implements OnInit {
|
|||
_self.RestrictionsTaskModalVisible = true;
|
||||
});
|
||||
}
|
||||
delete_ip(item:string){
|
||||
|
||||
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!=item);
|
||||
delete_ip(item: string) {
|
||||
|
||||
this.userresttrictions['allowed_ips'] = this.userresttrictions['allowed_ips'].filter((x: any) => x != item);
|
||||
}
|
||||
|
||||
add_ip(){
|
||||
add_ip() {
|
||||
//check if ip address is valid cidr and not added before
|
||||
let ip=this.ipaddress.trim();
|
||||
if(ip=="")return;
|
||||
if(this.userresttrictions['allowed_ips'].includes(ip)){
|
||||
let ip = this.ipaddress.trim();
|
||||
if (ip == "") return;
|
||||
if (this.userresttrictions['allowed_ips'].includes(ip)) {
|
||||
this.show_toast("Error", "IP already added", "danger");
|
||||
return;
|
||||
}
|
||||
//check if ip is valid cidr ip
|
||||
if(this.checkIpAddress(ip)){
|
||||
if (this.checkIpAddress(ip)) {
|
||||
this.userresttrictions['allowed_ips'].push(ip);
|
||||
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!="");
|
||||
this.ipaddress="";
|
||||
this.userresttrictions['allowed_ips'] = this.userresttrictions['allowed_ips'].filter((x: any) => x != "");
|
||||
this.ipaddress = "";
|
||||
}
|
||||
else{
|
||||
else {
|
||||
this.show_toast("Error", "Invalid IP address", "danger");
|
||||
}
|
||||
}
|
||||
|
||||
save_sec(){
|
||||
var _self=this;
|
||||
this.data_provider.save_user_restrictions(this.SelectedUser.id,this.userresttrictions).then((res) => {
|
||||
|
||||
save_sec() {
|
||||
var _self = this;
|
||||
this.data_provider.save_user_restrictions(this.SelectedUser.id, this.userresttrictions).then((res) => {
|
||||
if ("error" in res && res.error.indexOf("Unauthorized")) {
|
||||
_self.show_toast(
|
||||
"Error",
|
||||
|
|
@ -364,17 +340,17 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
if('status' in res && res['status']=='success')
|
||||
else {
|
||||
if ('status' in res && res['status'] == 'success')
|
||||
this.RestrictionsTaskModalVisible = false;
|
||||
else if('status' in res && res['status']=='failed')
|
||||
else if ('status' in res && res['status'] == 'failed')
|
||||
this.show_toast("Error", res.err, "danger");
|
||||
else
|
||||
else
|
||||
this.show_toast("Error", "Somthing went wrong", "danger");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
add_user_perm() {
|
||||
var _self = this;
|
||||
this.data_provider
|
||||
|
|
@ -391,10 +367,10 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.get_user_perms(_self.SelectedUser["id"]);
|
||||
_self.permission = 0;
|
||||
_self.devgroup = 0;
|
||||
else {
|
||||
_self.get_user_perms(_self.SelectedUser["id"]);
|
||||
_self.permission = 0;
|
||||
_self.devgroup = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -425,9 +401,12 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.initGridTable();
|
||||
_self.DeleteConfirmModalVisible = false;
|
||||
else if ("err" in res) {
|
||||
_self.show_toast("Error", res.err, "danger");
|
||||
}
|
||||
else {
|
||||
_self.initGridTable();
|
||||
_self.DeleteConfirmModalVisible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -451,12 +430,12 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
this.get_user_perms(this.SelectedUser["id"]);
|
||||
else {
|
||||
this.get_user_perms(this.SelectedUser["id"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
logger(item: any) {
|
||||
console.dir(item);
|
||||
}
|
||||
|
|
@ -474,13 +453,13 @@ export class UserManagerComponent implements OnInit {
|
|||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.source = res.map((x: any) => {
|
||||
return x;
|
||||
});
|
||||
_self.SelectedUser = {};
|
||||
_self.loading = false;
|
||||
}
|
||||
else {
|
||||
_self.source = res.map((x: any) => {
|
||||
return x;
|
||||
});
|
||||
_self.SelectedUser = {};
|
||||
_self.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ import { NgxMatSelectSearchModule } from "ngx-mat-select-search";
|
|||
|
||||
import { UserManagerRoutingModule } from "./user_manager-routing.module";
|
||||
import { UserManagerComponent } from "./user_manager.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -29,7 +31,9 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
TableModule,
|
||||
InputTextModule,
|
||||
TooltipModule,
|
||||
ModalModule,
|
||||
FormsModule,
|
||||
ToastModule,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,8 @@
|
|||
import { Component, OnInit, OnDestroy, QueryList, ViewChildren } from "@angular/core";
|
||||
import { Component, OnInit, OnDestroy, QueryList, ViewChildren, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSelectedRow,
|
||||
GuiSearching,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
|
|
@ -25,10 +14,13 @@ import { ToasterComponent } from "@coreui/angular";
|
|||
styleUrls: ["user_tasks.component.scss"]
|
||||
})
|
||||
export class UserTasksComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public ispro: boolean = false;
|
||||
|
||||
@ViewChild('dt') table!: Table;
|
||||
@ViewChild('dtNewMembers') tableNewMembers!: Table;
|
||||
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
|
|
@ -67,7 +59,6 @@ export class UserTasksComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public SelectedTask: any = {};
|
||||
|
|
@ -77,6 +68,7 @@ export class UserTasksComponent implements OnInit {
|
|||
public DeleteConfirmModalVisible: boolean = false;
|
||||
public Members: any = "";
|
||||
public Snippets: any;
|
||||
public Sequences: any = [];
|
||||
public SelectedMembers: any = [];
|
||||
public NewMemberModalVisible: boolean = false;
|
||||
public availbleMembers: any = [];
|
||||
|
|
@ -96,7 +88,7 @@ export class UserTasksComponent implements OnInit {
|
|||
{ label: 'Every 10 minutes', value: '*/10 * * * *', description: 'Regular monitoring checks' },
|
||||
{ label: 'Every 15 minutes', value: '*/15 * * * *', description: 'Moderate monitoring frequency' },
|
||||
{ label: 'Every 30 minutes', value: '*/30 * * * *', description: 'Low frequency monitoring' },
|
||||
|
||||
|
||||
// Hourly Operations
|
||||
{ label: 'Every hour', value: '0 * * * *', description: 'Hourly network checks' },
|
||||
{ label: 'Every 2 hours', value: '0 */2 * * *', description: 'Bi-hourly operations' },
|
||||
|
|
@ -104,7 +96,7 @@ export class UserTasksComponent implements OnInit {
|
|||
{ label: 'Every 6 hours', value: '0 */6 * * *', description: 'Four times daily' },
|
||||
{ label: 'Every 8 hours', value: '0 */8 * * *', description: 'Three times daily' },
|
||||
{ label: 'Every 12 hours', value: '0 */12 * * *', description: 'Twice daily operations' },
|
||||
|
||||
|
||||
// Daily Maintenance
|
||||
{ label: 'Daily at midnight', value: '0 0 * * *', description: 'Daily maintenance at 00:00' },
|
||||
{ label: 'Daily at 1 AM', value: '0 1 * * *', description: 'Daily backup at 01:00' },
|
||||
|
|
@ -113,20 +105,20 @@ export class UserTasksComponent implements OnInit {
|
|||
{ label: 'Daily at 6 AM', value: '0 6 * * *', description: 'Pre-business hours check' },
|
||||
{ label: 'Daily at 6 PM', value: '0 18 * * *', description: 'End of business day backup' },
|
||||
{ label: 'Daily at 10 PM', value: '0 22 * * *', description: 'Evening maintenance at 22:00' },
|
||||
|
||||
|
||||
// Business Hours
|
||||
{ label: 'Workdays at 8 AM', value: '0 8 * * 1-5', description: 'Start of business day - Mon to Fri' },
|
||||
{ label: 'Workdays at 9 AM', value: '0 9 * * 1-5', description: 'Business hours start check' },
|
||||
{ label: 'Workdays at 12 PM', value: '0 12 * * 1-5', description: 'Midday check - Mon to Fri' },
|
||||
{ label: 'Workdays at 5 PM', value: '0 17 * * 1-5', description: 'End of business day - Mon to Fri' },
|
||||
{ label: 'Workdays at 6 PM', value: '0 18 * * 1-5', description: 'After hours backup - Mon to Fri' },
|
||||
|
||||
|
||||
// Weekly Operations
|
||||
{ label: 'Weekly (Sunday midnight)', value: '0 0 * * 0', description: 'Weekly maintenance - Sunday 00:00' },
|
||||
{ label: 'Weekly (Monday midnight)', value: '0 0 * * 1', description: 'Weekly start - Monday 00:00' },
|
||||
{ label: 'Weekly (Friday 6 PM)', value: '0 18 * * 5', description: 'End of week backup - Friday 18:00' },
|
||||
{ label: 'Weekly (Saturday 2 AM)', value: '0 2 * * 6', description: 'Weekend maintenance - Saturday 02:00' },
|
||||
|
||||
|
||||
// Monthly Operations
|
||||
{ label: 'Monthly (1st at midnight)', value: '0 0 1 * *', description: 'Monthly maintenance - 1st of month' },
|
||||
{ label: 'Monthly (1st at 2 AM)', value: '0 2 1 * *', description: 'Monthly backup - 1st at 02:00' },
|
||||
|
|
@ -137,14 +129,13 @@ export class UserTasksComponent implements OnInit {
|
|||
public cronSearch: string = '';
|
||||
public selectedCronPreset: any = null;
|
||||
public filteredCrons: any[] = [];
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
applyFilterNewMembers($event: any, stringVal: string) {
|
||||
this.tableNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
options: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
|
|
@ -156,32 +147,16 @@ export class UserTasksComponent implements OnInit {
|
|||
enableDarkMode: false,
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
seqOptions: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Sequence",
|
||||
searchEnabled: true,
|
||||
enableDarkMode: false,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
|
||||
show_new_member_form() {
|
||||
this.NewMemberModalVisible = true;
|
||||
|
|
@ -256,9 +231,9 @@ export class UserTasksComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRowsNewMembers(rows: any[]): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source);
|
||||
this.SelectedNewMemberRows = rows;
|
||||
}
|
||||
|
||||
add_new_members() {
|
||||
|
|
@ -299,15 +274,17 @@ export class UserTasksComponent implements OnInit {
|
|||
|
||||
var _self = this;
|
||||
this.SelectedTask = { ...item };
|
||||
|
||||
|
||||
// Initialize cron search and preset tracking
|
||||
this.cronSearch = '';
|
||||
const currentCron = this.SelectedTask['cron'];
|
||||
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
|
||||
|
||||
if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) {
|
||||
this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']);
|
||||
if (this.SelectedTask['data']['strategy'] == 'defined') {
|
||||
|
||||
if ((this.SelectedTask['task_type'] == 'firmware' || this.SelectedTask['task_type'] == 'sequence') && 'data' in this.SelectedTask && this.SelectedTask['data']) {
|
||||
if (typeof this.SelectedTask['data'] === 'string') {
|
||||
this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']);
|
||||
}
|
||||
if (this.SelectedTask['task_type'] == 'firmware' && this.SelectedTask['data']['strategy'] == 'defined') {
|
||||
this.data_provider.get_firms(0, 10000, false).then((res) => {
|
||||
let index = 1;
|
||||
_self.available_firmwares = [
|
||||
|
|
@ -328,16 +305,23 @@ export class UserTasksComponent implements OnInit {
|
|||
_self.firms_loaded = true;
|
||||
});
|
||||
}
|
||||
else{
|
||||
else {
|
||||
_self.firms_loaded = true;
|
||||
}
|
||||
|
||||
}
|
||||
_self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => {
|
||||
_self.data_provider.get_snippets("", "", "", 0, 1000, false).then((res) => {
|
||||
_self.Snippets = res.map((x: any) => {
|
||||
return { id: x.id, name: x.name };
|
||||
});
|
||||
});
|
||||
if (_self.ispro) {
|
||||
_self.data_provider.get_sequences().then((res: any) => {
|
||||
_self.Sequences = res.map((x: any) => {
|
||||
return { id: x.id, name: x.name };
|
||||
});
|
||||
});
|
||||
}
|
||||
if (action != "select_change") {
|
||||
this.SelectedTask["action"] = "edit";
|
||||
this.data_provider.get_task_members(_self.SelectedTask.id).then((res) => {
|
||||
|
|
@ -354,7 +338,7 @@ export class UserTasksComponent implements OnInit {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
firmware_type_changed(type: any) {
|
||||
this.SelectedTask['data']['strategy'] = type;
|
||||
if (type == 'system') {
|
||||
|
|
@ -405,7 +389,7 @@ export class UserTasksComponent implements OnInit {
|
|||
onSnippetsValueChanged(v: any) {
|
||||
var _self = this;
|
||||
if (v == "" || v.length < 3) return;
|
||||
_self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => {
|
||||
_self.data_provider.get_snippets(v, "", "", 0, 1000, false).then((res) => {
|
||||
_self.Snippets = res.map((x: any) => {
|
||||
return { id: String(x.id), name: x.name };
|
||||
});
|
||||
|
|
@ -470,10 +454,10 @@ export class UserTasksComponent implements OnInit {
|
|||
const searchTerm = event.target.value.toLowerCase();
|
||||
this.cronSearch = searchTerm;
|
||||
this.selectedCronPreset = null;
|
||||
|
||||
|
||||
if (searchTerm.length > 0) {
|
||||
this.filteredCrons = this.predefinedCrons.filter(cron =>
|
||||
cron.label.toLowerCase().includes(searchTerm) ||
|
||||
this.filteredCrons = this.predefinedCrons.filter(cron =>
|
||||
cron.label.toLowerCase().includes(searchTerm) ||
|
||||
cron.description.toLowerCase().includes(searchTerm) ||
|
||||
cron.value.includes(searchTerm)
|
||||
);
|
||||
|
|
@ -491,7 +475,7 @@ export class UserTasksComponent implements OnInit {
|
|||
onCronInputFocus(): void {
|
||||
this.filteredCrons = this.predefinedCrons;
|
||||
this.showCronDropdown = true;
|
||||
|
||||
|
||||
// If current cron matches a preset, highlight it
|
||||
const currentCron = this.SelectedTask['cron'];
|
||||
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
|
||||
|
|
@ -500,7 +484,7 @@ export class UserTasksComponent implements OnInit {
|
|||
onCronInputChange(event: any): void {
|
||||
this.SelectedTask['cron'] = event.target.value;
|
||||
this.selectedCronPreset = null;
|
||||
|
||||
|
||||
// Check if the entered value matches any preset
|
||||
const enteredValue = event.target.value;
|
||||
const matchingPreset = this.predefinedCrons.find(cron => cron.value === enteredValue);
|
||||
|
|
@ -513,7 +497,7 @@ export class UserTasksComponent implements OnInit {
|
|||
if (this.selectedCronPreset) {
|
||||
return this.selectedCronPreset.description;
|
||||
}
|
||||
|
||||
|
||||
const currentCron = this.SelectedTask['cron'];
|
||||
const matchingPreset = this.predefinedCrons.find(cron => cron.value === currentCron);
|
||||
return matchingPreset ? matchingPreset.description : 'Custom cron expression';
|
||||
|
|
@ -522,6 +506,8 @@ export class UserTasksComponent implements OnInit {
|
|||
onTaskTypeChange(): void {
|
||||
if (this.SelectedTask['task_type'] === 'snippet') {
|
||||
this.loadSnippets();
|
||||
} else if (this.SelectedTask['task_type'] === 'sequence') {
|
||||
this.loadSequences();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -533,4 +519,22 @@ export class UserTasksComponent implements OnInit {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadSequences(): void {
|
||||
var _self = this;
|
||||
_self.data_provider.get_sequences().then((res: any) => {
|
||||
_self.Sequences = res.map((x: any) => {
|
||||
return { id: x.id, name: x.name };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onSequenceSelected($event: any) {
|
||||
if (!this.SelectedTask['data']) this.SelectedTask['data'] = {};
|
||||
this.SelectedTask['data']['sequence_id'] = $event;
|
||||
}
|
||||
|
||||
onSequencesSearchChanged(v: any) {
|
||||
// Sequences are fully loaded on open, client side filtering can be used or ignored for now
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import {
|
|||
} from "@coreui/angular";
|
||||
import { UserTasksRoutingModule } from "./user_tasks-routing.module";
|
||||
import { UserTasksComponent } from "./user_tasks.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
import { NgxSuperSelectModule} from "ngx-super-select";
|
||||
|
||||
|
|
@ -31,7 +33,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
|
|||
BadgeModule,
|
||||
AlertModule,
|
||||
ToastModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
ModalModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
|
|
|
|||
|
|
@ -1,22 +1,26 @@
|
|||
<div style="position: relative;">
|
||||
<c-card class="mb-4" style="margin-bottom: 0 !important;">
|
||||
<c-card-header style="padding: 0;">
|
||||
<c-row class="align-items-center">
|
||||
<c-col xs="12" md="6">
|
||||
<div class="nav nav-underline bg-transparent">
|
||||
<div class="nav-item">
|
||||
<a [active]="activetab==0" class="nav-link" [cTabContent]="tabContent" (click)="activetab=0" [tabPaneIdx]="0">
|
||||
<a [active]="activetab==0" class="nav-link" [cTabContent]="tabContent" (click)="activetab=0"
|
||||
[tabPaneIdx]="0">
|
||||
<i class="fas fa-cog me-1"></i>Settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a [active]="activetab==1" class="nav-link" [cTabContent]="tabContent" (click)="get_passwords();activetab=1" [tabPaneIdx]="1">
|
||||
<a [active]="activetab==1" class="nav-link" [cTabContent]="tabContent" (click)="get_passwords();activetab=1"
|
||||
[tabPaneIdx]="1">
|
||||
<i class="fas fa-key me-1"></i>Passwords
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</c-col>
|
||||
<c-col xs="12" md="6" class="text-end mt-2 mt-md-0">
|
||||
<button *ngIf="activetab==0" cButton size="sm" class="me-2" (click)="runConfirmModalVisible=!runConfirmModalVisible" color="danger">
|
||||
<button *ngIf="activetab==0" cButton size="sm" class="me-2"
|
||||
(click)="runConfirmModalVisible=!runConfirmModalVisible" color="danger">
|
||||
<i class="fas fa-play me-1"></i>Execute Now
|
||||
</button>
|
||||
<button *ngIf="activetab==1" cButton size="sm" (click)="toggleCollapse()" color="info">
|
||||
|
|
@ -36,11 +40,12 @@
|
|||
<i class="fas fa-cog text-primary me-2 fs-4"></i>
|
||||
<div>
|
||||
<h5 class="mb-1">Password Vault Settings</h5>
|
||||
<p class="text-muted mb-0 small">Configure automated password management for your device groups. Set schedules, define password policies, and manage affected devices.</p>
|
||||
<p class="text-muted mb-0 small">Configure automated password management for your device groups. Set
|
||||
schedules, define password policies, and manage affected devices.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Password Vault Configuration -->
|
||||
<div class="mb-4 pb-4 border-bottom">
|
||||
<h6 class="mb-3"><i class="fas fa-shield-alt me-2"></i>Password Vault Configuration</h6>
|
||||
|
|
@ -54,7 +59,8 @@
|
|||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="6" lg="3">
|
||||
<label class="form-label small">Strategy <i class="fas fa-info-circle text-info ms-1" [cTooltip]="strategyTooltipTemplate" cTooltipPlacement="top"></i></label>
|
||||
<label class="form-label small">Strategy <i class="fas fa-info-circle text-info ms-1"
|
||||
[cTooltip]="strategyTooltipTemplate" cTooltipPlacement="top"></i></label>
|
||||
<select cSelect size="sm" [(ngModel)]="settings['strategy']" class="form-select-sm">
|
||||
<option>Choose...</option>
|
||||
<option value="all">All Local Users</option>
|
||||
|
|
@ -62,7 +68,8 @@
|
|||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="6" lg="3">
|
||||
<label class="form-label small">Interval <i class="fas fa-info-circle text-info ms-1" [cTooltip]="intervalTooltipTemplate" cTooltipPlacement="top"></i></label>
|
||||
<label class="form-label small">Interval <i class="fas fa-info-circle text-info ms-1"
|
||||
[cTooltip]="intervalTooltipTemplate" cTooltipPlacement="top"></i></label>
|
||||
<select cSelect size="sm" [(ngModel)]="settings['interval']" class="form-select-sm">
|
||||
<option>Choose...</option>
|
||||
<option value="daily">Daily</option>
|
||||
|
|
@ -74,7 +81,8 @@
|
|||
</select>
|
||||
</c-col>
|
||||
<c-col xs="12" md="6" lg="3">
|
||||
<label class="form-label small">Password Type <i class="fas fa-info-circle text-info ms-1" [cTooltip]="passwordTypeTooltipTemplate" cTooltipPlacement="top"></i></label>
|
||||
<label class="form-label small">Password Type <i class="fas fa-info-circle text-info ms-1"
|
||||
[cTooltip]="passwordTypeTooltipTemplate" cTooltipPlacement="top"></i></label>
|
||||
<select cSelect size="sm" [(ngModel)]="settings['password_type']" class="form-select-sm">
|
||||
<option>Choose...</option>
|
||||
<option value="random">Random</option>
|
||||
|
|
@ -104,20 +112,26 @@
|
|||
</c-col>
|
||||
</c-row>
|
||||
<div *ngIf="settings['exceptions']?.length > 0">
|
||||
<gui-grid [autoResizeWidth]="true" [source]="settings['exceptions']" [columnMenu]="columnMenu" [paging]="paging" [sorting]="sorting">
|
||||
<gui-grid-column header="Username" field="name">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
<i class="fas fa-user me-2 text-muted"></i>{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="80" field="action" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button (click)="remove_exception(item)" cButton color="danger" size="sm" variant="outline">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table [value]="settings['exceptions']" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name">Username <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th style="width: 80px" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><i class="fas fa-user me-2 text-muted"></i>{{item}}</td>
|
||||
<td class="text-center">
|
||||
<button (click)="remove_exception(item)" cButton color="danger" size="sm" variant="outline"
|
||||
pTooltip="Remove Exception">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pre-defined Passwords -->
|
||||
|
|
@ -137,20 +151,26 @@
|
|||
</c-col>
|
||||
</c-row>
|
||||
<div *ngIf="settings['passwords']?.length > 0">
|
||||
<gui-grid [autoResizeWidth]="true" [source]="settings['passwords']" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging">
|
||||
<gui-grid-column header="Password" field="name">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
<i class="fas fa-lock me-2 text-muted"></i>••••••••
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="80" field="action" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="remove_password(item)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table [value]="settings['passwords']" [showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name">Password <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th style="width: 80px" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><i class="fas fa-lock me-2 text-muted"></i>••••••••</td>
|
||||
<td class="text-center">
|
||||
<button cButton color="danger" size="sm" variant="outline" (click)="remove_password(item)"
|
||||
pTooltip="Remove Password">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Affected Groups -->
|
||||
|
|
@ -167,20 +187,25 @@
|
|||
</c-col>
|
||||
</c-row>
|
||||
<div *ngIf="Members?.length > 0" class="mb-3">
|
||||
<gui-grid [autoResizeWidth]="true" [source]="Members" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging">
|
||||
<gui-grid-column header="Group Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<i class="fas fa-server me-2 text-primary"></i>{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="80" field="action" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button (click)="delete_group(item.id)" cButton color="danger" size="sm" variant="outline">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table [value]="Members" [showGridlines]="true" [stripedRows]="true" styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name">Group Name <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th style="width: 80px" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><i class="fas fa-server me-2 text-primary"></i>{{item.name}}</td>
|
||||
<td class="text-center">
|
||||
<button (click)="delete_group(item.id)" cButton color="danger" size="sm" variant="outline"
|
||||
pTooltip="Remove Group">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<button cButton color="success" (click)="save_settings()">
|
||||
|
|
@ -192,25 +217,28 @@
|
|||
<!-- Execution History -->
|
||||
<div *ngIf="vault_history">
|
||||
<h6 class="mb-3"><i class="fas fa-history me-2"></i>Execution Reports</h6>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="vault_history" [columnMenu]="columnMenu" [sorting]="sorting" [paging]="paging">
|
||||
<gui-grid-column header="Start Time" field="started">
|
||||
<ng-template let-value="item.started" let-item="item" let-index="index">
|
||||
<small>{{value}}</small>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="End Time" field="ended">
|
||||
<ng-template let-value="item.ended" let-item="item" let-index="index">
|
||||
<small>{{value}}</small>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Results" field="result" align="center" width="100">
|
||||
<ng-template let-value="item['result']" let-item="item" let-index="index">
|
||||
<button (click)="exportToCsv(value)" color="primary" size="sm" cButton variant="outline">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<p-table #dtHistory [value]="vault_history" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" styleClass="p-datatable-sm">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="started">Start Time <p-sortIcon field="started"></p-sortIcon></th>
|
||||
<th pSortableColumn="ended">End Time <p-sortIcon field="ended"></p-sortIcon></th>
|
||||
<th style="width: 100px" class="text-center">Results</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><small>{{item.started}}</small></td>
|
||||
<td><small>{{item.ended}}</small></td>
|
||||
<td class="text-center">
|
||||
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton variant="outline"
|
||||
pTooltip="Download CSV">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
|
|
@ -224,11 +252,12 @@
|
|||
<i class="fas fa-key text-warning me-2 fs-4"></i>
|
||||
<div>
|
||||
<h5 class="mb-1">Stored Passwords</h5>
|
||||
<p class="text-muted mb-0 small">View and manage passwords stored by the vault system. Use filters to find specific devices or users.</p>
|
||||
<p class="text-muted mb-0 small">View and manage passwords stored by the vault system. Use filters to find
|
||||
specific devices or users.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 pb-4 border-bottom" [visible]="filters_visible" cCollapse>
|
||||
<h6 class="mb-3"><i class="fas fa-filter me-2"></i>Filters</h6>
|
||||
|
|
@ -257,41 +286,59 @@
|
|||
<!-- Stored Passwords -->
|
||||
<div *ngIf="passwords">
|
||||
<h6 class="mb-3"><i class="fas fa-database me-2"></i>Stored Passwords</h6>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="passwords" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel">
|
||||
<gui-grid-column header="Device" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-router me-2 text-primary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{value}}</div>
|
||||
<small class="text-muted">{{item.devip}}</small>
|
||||
<p-table #dtPasswords [value]="passwords" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name', 'devip', 'username', 'changed']" [loading]="loading">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
Device
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="username" pResizableColumn>
|
||||
User
|
||||
<p-sortIcon field="username"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="username" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="changed" pResizableColumn>
|
||||
Last Changed
|
||||
<p-sortIcon field="changed"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="changed" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th style="width: 100px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-server me-2 text-primary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{item.name}}</div>
|
||||
<small class="text-muted">{{item.devip}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Username" field="username">
|
||||
<ng-template let-value="item.username" let-item="item" let-index="index">
|
||||
<i class="fas fa-user me-2 text-muted"></i>{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Last Changed" field="changed">
|
||||
<ng-template let-value="item.changed" let-item="item" let-index="index">
|
||||
<small>{{value}}</small>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="100" field="action" align="center">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton *ngIf="ispro" (click)="reveal_password(item.devid,item.username)" color="info" size="sm" variant="outline">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</td>
|
||||
<td><i class="fas fa-user me-2 text-muted"></i>{{item.username}}</td>
|
||||
<td><small>{{item.changed}}</small></td>
|
||||
<td class="text-center">
|
||||
<button cButton *ngIf="ispro" (click)="reveal_password(item.devid,item.username)" color="info"
|
||||
size="sm" variant="outline" pTooltip="View Password">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-tab-pane>
|
||||
</c-tab-content>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
|
||||
<!-- Password Reveal Modal -->
|
||||
<c-modal #PasswordModal backdrop="static" [(visible)]="PasswordModalVisible" id="PasswordModal">
|
||||
|
|
@ -306,7 +353,7 @@
|
|||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
<input [value]="password" cFormControl disabled="true" class="form-control"/>
|
||||
<input [value]="password" cFormControl disabled="true" class="form-control" />
|
||||
<button cButton color="secondary" variant="outline" (click)="copyToClipboard(password)">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
|
|
@ -325,7 +372,8 @@
|
|||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div class="alert alert-warning">
|
||||
<strong>Warning:</strong> This will execute the password vault job and change passwords on all configured device groups.
|
||||
<strong>Warning:</strong> This will execute the password vault job and change passwords on all configured device
|
||||
groups.
|
||||
</div>
|
||||
<p>Are you sure you want to proceed with the password vault execution?</p>
|
||||
</c-modal-body>
|
||||
|
|
@ -345,25 +393,50 @@
|
|||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p class="text-muted mb-3">Select device groups to include in the password vault task:</p>
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
|
||||
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging">
|
||||
<gui-grid-column header="Group Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<i class="fas fa-server me-2 text-primary"></i>{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="MAC Address" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search groups..." (input)="applyFilterNewMembers($event, 'contains')"
|
||||
class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dtNewMembers [value]="availbleMembers" [paginator]="true" [rows]="10" [showGridlines]="true"
|
||||
[stripedRows]="true" [resizableColumns]="true" columnResizeMode="expand"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped" [(selection)]="NewMemberRows"
|
||||
(selectionChange)="onSelectedRowsNewMembers($event)" [globalFilterFields]="['name', 'ip', 'mac']">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 4rem" pResizableColumn><p-tableHeaderCheckbox></p-tableHeaderCheckbox></th>
|
||||
<th pSortableColumn="name" pResizableColumn>
|
||||
Name
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="ip" pResizableColumn>
|
||||
IP
|
||||
<p-sortIcon field="ip"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="ip" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
<th pSortableColumn="mac" pResizableColumn>
|
||||
MAC
|
||||
<p-sortIcon field="mac"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="mac" display="menu" class="ms-auto" />
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr>
|
||||
<td><p-tableCheckbox [value]="item"></p-tableCheckbox></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-server me-2 text-primary"></i>
|
||||
<strong>{{item.name}}</strong>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="NewMemberRows.length != 0" (click)="add_new_members()" cButton color="primary">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
import { Component, OnInit, ViewChildren ,QueryList} from "@angular/core";
|
||||
import { Component, OnInit, ViewChildren, QueryList, ViewChild } from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSelectedRow,
|
||||
GuiSearching,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { Table } from 'primeng/table';
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
|
@ -27,10 +16,14 @@ import { AppToastComponent } from "../toast-simple/toast.component";
|
|||
|
||||
})
|
||||
export class VaultComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public uid: number = 0;
|
||||
public uname: string = '';
|
||||
public ispro: boolean = false;
|
||||
public tz: string;
|
||||
public tz: string = '';
|
||||
|
||||
@ViewChild('dtPasswords') dtPasswords!: Table;
|
||||
@ViewChild('dtHistory') dtHistory!: Table;
|
||||
@ViewChild('dtNewMembers') dtNewMembers!: Table;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
|
|
@ -71,7 +64,6 @@ export class VaultComponent implements OnInit {
|
|||
public password:string="";
|
||||
public PasswordModalVisible:boolean=false;
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public SelectedTask: any = {};
|
||||
|
|
@ -87,16 +79,7 @@ export class VaultComponent implements OnInit {
|
|||
public filters: any = {};
|
||||
public activetab:number=0;
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
|
||||
toasterForm = {
|
||||
public toasterForm = {
|
||||
autohide: true,
|
||||
delay: 3000,
|
||||
position: "fixed",
|
||||
|
|
@ -104,42 +87,17 @@ export class VaultComponent implements OnInit {
|
|||
closeButton: true,
|
||||
};
|
||||
|
||||
options: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Snippet",
|
||||
searchEnabled: true,
|
||||
enableDarkMode: false,
|
||||
};
|
||||
applyFilterPasswords($event: any, stringVal: string) {
|
||||
this.dtPasswords.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
applyFilterHistory($event: any, stringVal: string) {
|
||||
this.dtHistory.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
applyFilterNewMembers($event: any, stringVal: string) {
|
||||
this.dtNewMembers.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
reinitgrid(field: string, $event: any) {
|
||||
if (field == "username") this.filters["username"] = $event;
|
||||
|
|
@ -153,10 +111,9 @@ export class VaultComponent implements OnInit {
|
|||
this.get_vault_history();
|
||||
}
|
||||
|
||||
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
|
||||
onSelectedRowsNewMembers(rows: any[]): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => ({'id': m.source.id,'name':m.source.name}));
|
||||
|
||||
this.SelectedNewMemberRows = rows.map((m: any) => ({'id': m.id,'name':m.name}));
|
||||
}
|
||||
|
||||
toggleCollapse(): void {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@ import {
|
|||
} from "@coreui/angular";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { InputTextModule as PInputTextModule } from 'primeng/inputtext';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
@NgModule({
|
||||
imports: [
|
||||
VaultRoutingModule,
|
||||
|
|
@ -29,7 +32,9 @@ import { MatFormFieldModule } from "@angular/material/form-field";
|
|||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
PTableModule,
|
||||
PInputTextModule,
|
||||
PTooltipModule,
|
||||
ModalModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
|
|
@ -38,7 +43,8 @@ import { MatFormFieldModule } from "@angular/material/form-field";
|
|||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
CollapseModule,
|
||||
TooltipModule
|
||||
TooltipModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [VaultComponent],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -63,12 +63,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Grid Improvements
|
||||
gui-grid {
|
||||
.gui-grid {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Input Group Compact
|
||||
.input-group-sm {
|
||||
|
|
@ -204,19 +198,6 @@ gui-grid {
|
|||
}
|
||||
}
|
||||
|
||||
// Grid Cell Improvements
|
||||
::ng-deep gui-grid {
|
||||
.gui-grid-cell {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.gui-grid-header-cell {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab Info Headers
|
||||
.tab-info-header {
|
||||
|
|
|
|||
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 {
|
||||
}
|
||||
656
src/app/views/vpn/vpn.component.html
Normal file
656
src/app/views/vpn/vpn.component.html
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
<div style="position: relative;">
|
||||
<div class="fade-in">
|
||||
<!-- Communication Error Alert -->
|
||||
<c-row class="mb-4" *ngIf="isCommunicationError">
|
||||
<c-col sm="12">
|
||||
<div class="alert alert-danger shadow-sm border-0 d-flex align-items-start p-4 mb-0">
|
||||
<div class="me-3 fs-2">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="alert-heading fw-bold">Need to check docker container and wireguard server</h5>
|
||||
<p class="mb-2">For this feature, the MikroWizard WireGuard server must be installed. If already installed,
|
||||
please verify that the Docker container is running and accessible.</p>
|
||||
<hr>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-2 text-muted fw-semibold">Troubleshooting and installation Instruction:</span>
|
||||
<a href="https://mikrowizard.com/docs/install-mikrowizard-wireguard-docker-server/" target="_blank"
|
||||
class="btn btn-danger btn-sm text-decoration-none">
|
||||
<i class="fas fa-external-link-alt me-1"></i> Install Mikrowizard wireguard docker server
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<!-- Top Cards -->
|
||||
<c-row class="mb-4 d-flex justify-content-between">
|
||||
<!-- Server Status Card -->
|
||||
<c-col sm="4">
|
||||
<c-card class="shadow-sm border-0 h-100"
|
||||
[ngClass]="{'bg-success text-white': status?.status === 'running' && !isCommunicationError, 'bg-danger text-white': status?.status === 'error' || isCommunicationError || !status, 'bg-warning text-dark': status?.status === 'setup_required' && !isCommunicationError}">
|
||||
<c-card-body class="p-3 d-flex align-items-center">
|
||||
<div class="me-3 fs-2 opacity-75">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-uppercase fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px; opacity: 0.8;">
|
||||
Server Status</div>
|
||||
<div class="fs-5 fw-bold mt-1">
|
||||
{{isCommunicationError ? 'Communication Failed' : (status?.status === 'running' ? 'Running' :
|
||||
(status?.status === 'setup_required' ? 'Setup Required' :
|
||||
'Error/Offline'))}}
|
||||
</div>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
||||
<!-- Total Peers Card -->
|
||||
<c-col sm="4">
|
||||
<c-card class="shadow-sm border-0 h-100 bg-primary text-white">
|
||||
<c-card-body class="p-3 d-flex align-items-center">
|
||||
<div class="me-3 fs-2 opacity-75">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-uppercase fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px; opacity: 0.8;">
|
||||
Connected Peers</div>
|
||||
<div class="fs-5 fw-bold mt-1">
|
||||
{{ source.length }} Active
|
||||
</div>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
|
||||
<!-- Total Traffic Card -->
|
||||
<c-col sm="4">
|
||||
<c-card class="shadow-sm border-0 h-100 bg-info text-white position-relative">
|
||||
<button class="btn btn-sm btn-light position-absolute top-0 end-0 m-2" style="opacity: 0.8; z-index: 2;"
|
||||
(click)="promptResetServer()" cTooltip="Reset Server Counters">
|
||||
<i class="fa-solid fa-redo"></i>
|
||||
</button>
|
||||
<c-card-body class="p-3 d-flex align-items-center">
|
||||
<div class="me-3 fs-3 opacity-75">
|
||||
<i class="fas fa-satellite-dish"></i>
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between text-uppercase fw-semibold"
|
||||
style="font-size: 0.70rem; letter-spacing: 0.5px; opacity: 0.8;">
|
||||
<span>Live Speed</span>
|
||||
<span>Total Vol</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1 w-100">
|
||||
<!-- Live Speeds -->
|
||||
<div class="d-flex flex-column" style="font-size: 0.8rem; font-weight: 500;">
|
||||
<span class="text-nowrap" [ngClass]="{'text-success': liveSpeedRx !== 0}">⬇ {{ formatBytes(liveSpeedRx)
|
||||
}}/s</span>
|
||||
<span class="text-nowrap" [ngClass]="{'text-warning': liveSpeedTx !== 0}">⬆ {{ formatBytes(liveSpeedTx)
|
||||
}}/s</span>
|
||||
</div>
|
||||
|
||||
<!-- Total Volumes -->
|
||||
<div class="d-flex flex-column text-end" style="font-size: 0.8rem; font-weight: 500;">
|
||||
<span class="text-nowrap">⬇ {{ (totalRx / 1048576) | number:'1.1-2' }} MB</span>
|
||||
<span class="text-nowrap">⬆ {{ (totalTx / 1048576) | number:'1.1-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
|
||||
<!-- Traffic Chart -->
|
||||
<c-row class="mb-4">
|
||||
<c-col sm="12">
|
||||
<c-card class="shadow-sm border-0">
|
||||
<c-card-header>
|
||||
<strong>Live Server Traffic</strong> <small class="text-muted">(Total Cumulative MB)</small>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<div class="chart-wrapper" style="height: 250px;">
|
||||
<c-chart [data]="chartData" [options]="chartOptions" type="line" height="250"></c-chart>
|
||||
</div>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
|
||||
<!-- Peers Table -->
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4">
|
||||
<c-card-header>
|
||||
<c-row>
|
||||
<c-col xs [lg]="3">
|
||||
<strong>VPN Peers</strong>
|
||||
</c-col>
|
||||
<c-col xs [lg]="9">
|
||||
<h6 style="text-align: right;">
|
||||
<button cButton color="primary" (click)="openServerConfigModal()" class="mx-1" size="sm">
|
||||
<i class="fas fa-cogs"></i> Server Config
|
||||
</button>
|
||||
<button cButton color="success" (click)="openAddPeerModal()" class="mx-1" size="sm"
|
||||
style="color: #fff;">
|
||||
<i class="fa-solid fa-plus"></i> Add Peer
|
||||
</button>
|
||||
</h6>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input type="text" pInputText placeholder="Search peers..."
|
||||
(input)="applyFilterGlobal($event, 'contains')" class="form-control-sm" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
||||
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
|
||||
[showGridlines]="true" [stripedRows]="true"
|
||||
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
||||
[globalFilterFields]="['name', 'assigned_ip', 'public_key', 'description']" [loading]="loading">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 50px" class="text-center" pResizableColumn>Status</th>
|
||||
<th pSortableColumn="name" style="width: 200px" pResizableColumn>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Identity & IP</span>
|
||||
<span>
|
||||
<p-sortIcon field="name"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="description" pResizableColumn>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Details & Description</span>
|
||||
<span>
|
||||
<p-sortIcon field="description"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="nat_mode" style="width: 140px" pResizableColumn>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Routing</span>
|
||||
<span>
|
||||
<p-sortIcon field="nat_mode"></p-sortIcon>
|
||||
<p-columnFilter type="text" field="nat_mode" display="menu" class="ms-auto" />
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 220px" class="text-center" pResizableColumn>Integration</th>
|
||||
<th pSortableColumn="stats.last_handshake" style="width: 130px" class="text-center" pResizableColumn>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Last Handshake</span>
|
||||
<p-sortIcon field="stats.last_handshake"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 130px" pResizableColumn>Transfer Speed</th>
|
||||
<th style="width: 120px" pResizableColumn>Total Volume</th>
|
||||
<th pSortableColumn="is_enabled" style="width: 90px" class="text-center" pResizableColumn>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>State</span>
|
||||
<p-sortIcon field="is_enabled"></p-sortIcon>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 70px" class="text-center" pResizableColumn>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-item>
|
||||
<tr [ngClass]="{'row-highlighted': item.status === 'online'}">
|
||||
<td class="text-center">
|
||||
<i *ngIf="item.status === 'online'" class="fa-solid fa-circle text-success" [pTooltip]="'Online'"
|
||||
tooltipPosition="top"></i>
|
||||
<i *ngIf="item.status === 'offline'" class="fa-solid fa-circle text-danger" [pTooltip]="'Offline'"
|
||||
tooltipPosition="top"></i>
|
||||
<i *ngIf="item.status === 'unreachable'" class="fa-solid fa-triangle-exclamation text-warning"
|
||||
[pTooltip]="'Unreachable (Handshake OK, Ping Failed)'" tooltipPosition="top"></i>
|
||||
<i *ngIf="!item.status && item.is_enabled" class="fa-solid fa-circle text-info" [pTooltip]="'Enabled'"
|
||||
tooltipPosition="top"></i>
|
||||
<i *ngIf="!item.status && !item.is_enabled" class="fa-solid fa-circle text-secondary"
|
||||
[pTooltip]="'Disabled'" tooltipPosition="top"></i>
|
||||
</td>
|
||||
<td>
|
||||
<div style="line-height: 1.2;">
|
||||
<i *ngIf="item.mt_user" class="fas fa-server text-info me-1" [pTooltip]="'Managed MikroTik Device'"
|
||||
style="vertical-align: middle; font-size: 0.9rem;"></i>
|
||||
<strong class="text-primary fs-6 text-truncate d-inline-block"
|
||||
style="max-width: 140px; vertical-align: middle;" [title]="item.name || item.assigned_ip">
|
||||
{{ item.name || item.assigned_ip || (item.public_key | slice:0:8) + '...' }}
|
||||
</strong>
|
||||
<c-badge *ngIf="item.is_managed === false" color="secondary" class="ms-1"
|
||||
style="vertical-align: middle; font-size: 0.65rem;">Unmanaged</c-badge>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: #495057;" class="fw-semibold ms-2">
|
||||
<i class="fas fa-network-wired opacity-75"></i> {{ item.assigned_ip || 'No IP' }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column justify-content-center w-100">
|
||||
<div *ngIf="item.description" class="text-muted mb-1 text-wrap small fw-bold"
|
||||
[title]="item.description">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div style="color: #adb5bd; font-size: 0.75rem" class="d-flex flex-row align-items-center gap-3">
|
||||
<div class="text-truncate" title="Public Key" style="max-width: 140px;">
|
||||
<i class="fas fa-key me-1 opacity-75"></i> {{ item.public_key | slice:0:16 }}...
|
||||
</div>
|
||||
<div class="text-truncate" title="Created At">
|
||||
<i class="fas fa-clock me-1 opacity-75"></i> {{ item.created_at | date:'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mb-1">
|
||||
<c-badge size="sm"
|
||||
[color]="item.nat_mode === 'full' ? 'success' : (item.nat_mode === 'split' ? 'info' : 'secondary')">
|
||||
{{ item.nat_mode | uppercase }}
|
||||
</c-badge>
|
||||
</div>
|
||||
<div style="font-size: 0.75rem;" class="text-muted mt-1">
|
||||
<i class="fas fa-network-wired small opacity-50"></i> Iface: {{ item.custom_interface || 'Auto' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column align-items-center gap-1">
|
||||
<a *ngIf="item.linked_device_id" [routerLink]="['/device-stats', {id: item.linked_device_id}]"
|
||||
target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.7rem;">
|
||||
<i class="fa-solid fa-link"></i> View Device
|
||||
</a>
|
||||
<div class="d-flex flex-row justify-content-center w-100 container-fluid p-0">
|
||||
<small *ngIf="!item.linked_device_id && !item.scan_status && item.mt_user" class="text-muted"
|
||||
style="font-size: 0.65rem;">Not Linked</small>
|
||||
<div style="font-size: 0.7rem;"
|
||||
*ngIf="item.scan_status === 'starting' || item.scan_status === 'running'" class="text-info">
|
||||
<i class="fas fa-circle-notch fa-spin me-1"></i> Scanning...
|
||||
</div>
|
||||
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'completed'" class="text-success">
|
||||
<i class="fas fa-check-circle me-1"></i> Success
|
||||
</div>
|
||||
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'failed'" class="text-danger"
|
||||
[pTooltip]="'MikroTik connection failed'" tooltipPosition="top">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i> Failed
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 0.65rem;" class="text-secondary opacity-75">
|
||||
<i class="fas fa-heartbeat"></i> Keepalive: {{ item.persistent_keepalive }}s
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center small">
|
||||
<div *ngIf="item.stats?.last_handshake > 0" class="text-secondary fw-semibold">
|
||||
<i class="fas fa-handshake"></i> {{ item.stats.last_handshake * 1000 | date:'shortTime' }}<br>
|
||||
<small class="text-muted">{{ item.stats.last_handshake * 1000 | date:'shortDate' }}</small>
|
||||
</div>
|
||||
<div *ngIf="!item.stats?.last_handshake || item.stats.last_handshake === 0" class="text-muted">
|
||||
<i class="fas fa-handshake"></i> None
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div *ngIf="item.stats" class="d-flex flex-column"
|
||||
style="font-size: 0.72rem; font-family: monospace; font-weight: bold;">
|
||||
<span [style.color]="item.stats.rx_speed !== 0 ? '#2eb85c' : '#8a93a2'">
|
||||
<i class="fas fa-arrow-down opacity-75"></i> {{ formatBytes(item.stats.rx_speed || 0) }}/s
|
||||
</span>
|
||||
<span [style.color]="item.stats.tx_speed !== 0 ? '#3399ff' : '#8a93a2'">
|
||||
<i class="fas fa-arrow-up opacity-75"></i> {{ formatBytes(item.stats.tx_speed || 0) }}/s
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div *ngIf="item.stats" class="d-flex flex-column" style="font-size: 0.7rem;">
|
||||
<span class="text-success"><i class="fas fa-arrow-down opacity-50"></i> {{(item.stats.rx_bytes /
|
||||
1048576) | number:'1.2-2'}} MB</span>
|
||||
<span class="text-info"><i class="fas fa-arrow-up opacity-50"></i> {{(item.stats.tx_bytes / 1048576)
|
||||
| number:'1.2-2'}} MB</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<c-form-check [switch]="true" class="d-inline-block">
|
||||
<input cFormCheckInput type="checkbox" [checked]="item.is_enabled"
|
||||
(click)="$event.preventDefault(); promptToggleEnabled(item)"
|
||||
[disabled]="item.is_managed === false" [id]="'switch-peer-' + item.public_key" />
|
||||
</c-form-check>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
|
||||
[matMenuTriggerFor]="menu" cButton>
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<div cListGroup>
|
||||
<button size="sm" cListGroupItem (click)="openEditModal(item)" *ngIf="item.is_managed !== false">
|
||||
<i class="fa-solid fa-pencil text-primary"></i><small> Edit</small>
|
||||
</button>
|
||||
<button *ngIf="item.mt_user && !item.linked_device_id" size="sm" cListGroupItem
|
||||
(click)="scanDevice(item)">
|
||||
<i class="fa-solid fa-magnifying-glass text-info"></i><small> Manual Scan</small>
|
||||
</button>
|
||||
<button size="sm" cListGroupItem (click)="openScriptModal(item)">
|
||||
<i class="fa-solid fa-terminal text-secondary"></i><small> MikroTik Script</small>
|
||||
</button>
|
||||
<button size="sm" cListGroupItem (click)="openQrModal(item)">
|
||||
<i class="fa-solid fa-qrcode text-secondary"></i><small> QR Code</small>
|
||||
</button>
|
||||
<button size="sm" cListGroupItem (click)="downloadPeerConfigDirect(item)">
|
||||
<i class="fa-solid fa-download text-secondary"></i><small> Download Config</small>
|
||||
</button>
|
||||
<button size="sm" cListGroupItem (click)="promptResetPeer(item)">
|
||||
<i class="fa-solid fa-sync text-warning"></i><small> Reset Counters</small>
|
||||
</button>
|
||||
<button size="sm" cListGroupItem (click)="confirmDelete(item)">
|
||||
<i class="fa-solid fa-trash text-danger"></i><small> Delete</small>
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="10" class="text-center p-4">No VPN peers found.</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<!-- MODALS -->
|
||||
|
||||
<!-- Server Config Modal -->
|
||||
<c-modal #ServerConfigModal backdrop="static" [(visible)]="serverConfigModalVisible" id="ServerConfigModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>VPN Server Configuration</h6>
|
||||
<button [cModalToggle]="ServerConfigModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>API Endpoint</span>
|
||||
<input cFormControl [(ngModel)]="serverConfig.api_endpoint" placeholder="http://127.0.0.1:5000" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>API Token</span>
|
||||
<input cFormControl type="password" [(ngModel)]="serverConfig.api_token" placeholder="Optional Auth Token" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>VPN Subnet</span>
|
||||
<input cFormControl [(ngModel)]="serverConfig.vpn_subnet" placeholder="10.8.0.0/24" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>Public Server IP/Host</span>
|
||||
<input cFormControl [(ngModel)]="serverConfig.public_server_ip" placeholder="vpn.mikrowizard.com" />
|
||||
</c-input-group>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="primary" (click)="saveServerConfig()">Save Changes</button>
|
||||
<button cButton color="secondary" (click)="serverConfigModalVisible = false">Cancel</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Delete Confirm Modal -->
|
||||
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="deleteModalVisible" id="DeleteConfirmModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle class="text-danger">Warning: Delete Peer</h6>
|
||||
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p class="text-danger">
|
||||
<em>Warning: This will permanently remove the VPN tunnel and NAT rules for this peer. Connected devices will
|
||||
lose connection immediately.</em>
|
||||
</p>
|
||||
<p>Are you sure you want to delete peer <strong>{{ peerToDelete?.assigned_ip }}</strong>?</p>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="danger" (click)="executeDelete()">Yes, Delete Peer</button>
|
||||
<button cButton color="secondary" (click)="deleteModalVisible = false">Cancel</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Toggle Confirm Modal -->
|
||||
<c-modal #ToggleConfirmModal backdrop="static" [(visible)]="toggleModalVisible" id="ToggleConfirmModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Confirm Status Change</h6>
|
||||
<button [cModalToggle]="ToggleConfirmModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p>Are you sure you want to <strong [ngClass]="peerToToggle?.is_enabled ? 'text-danger' : 'text-success'">{{
|
||||
peerToToggle?.is_enabled ? 'Disable' : 'Enable' }}</strong> peer <strong class="text-primary">{{
|
||||
peerToToggle?.assigned_ip }}</strong>?</p>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton [color]="peerToToggle?.is_enabled ? 'danger' : 'success'" (click)="confirmToggle()">Yes, {{
|
||||
peerToToggle?.is_enabled ? 'Disable' : 'Enable' }}</button>
|
||||
<button cButton color="secondary" (click)="toggleModalVisible = false">Cancel</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Script Viewer Modal -->
|
||||
<c-modal #ScriptModal backdrop="static" [fullscreen]="true" [(visible)]="scriptModalVisible" id="ScriptModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>MikroTik Provisioning Script</h6>
|
||||
<button [cModalToggle]="ScriptModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div *ngIf="configResult.script">
|
||||
<div style="overflow-y: auto; background-color: #f8f9fa;">
|
||||
<div highlight-js lang="routeros" [options]="{}">{{
|
||||
configResult.script }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-end">
|
||||
<button cButton color="primary" [cdkCopyToClipboard]="configResult.script || ''" (click)="copyScript()">
|
||||
<i class="fas fa-copy"></i> Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="secondary" (click)="scriptModalVisible = false">Close</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- QR Viewer Modal -->
|
||||
<c-modal #QrModal backdrop="static" [(visible)]="qrModalVisible" id="QrModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Mobile Quick Setup (QR Code)</h6>
|
||||
<button [cModalToggle]="QrModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div *ngIf="configResult.qrBlobUrl" class="text-center mb-3">
|
||||
<img [src]="configResult.qrBlobUrl" alt="WireGuard QR Code"
|
||||
style="max-width: 250px; border: 1px solid #ddd; padding: 10px; border-radius: 8px;" />
|
||||
</div>
|
||||
<div class="alert alert-info py-2 m-0" style="font-size: 0.85rem;">
|
||||
<i class="fas fa-info-circle"></i> <strong>How to connect:</strong> Open the official WireGuard app on your
|
||||
mobile device, tap the <i class="fas fa-plus"></i> button, and select <strong>"Create from QR code"</strong>.
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer class="d-flex justify-content-between w-100">
|
||||
<div>
|
||||
<button cButton color="primary" variant="outline" size="sm" class="me-2"
|
||||
(click)="activePeerConfig && downloadPeerConfigDirect(activePeerConfig)">
|
||||
<i class="fas fa-download"></i> Download Config
|
||||
</button>
|
||||
<button cButton color="info" variant="outline" size="sm"
|
||||
(click)="activePeerConfig && openScriptModal(activePeerConfig)">
|
||||
<i class="fas fa-terminal"></i> Show Script
|
||||
</button>
|
||||
</div>
|
||||
<button cButton color="secondary" (click)="qrModalVisible = false">Close</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Add / Edit Peer Wizard Modal -->
|
||||
<c-modal #AddPeerModal backdrop="static" size="lg" [(visible)]="addPeerModalVisible" id="AddPeerModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>{{ editingPeer ? 'Edit VPN Peer' : 'Create New VPN Peer' }}</h5>
|
||||
<button [cModalToggle]="AddPeerModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<!-- Step 1: General Info -->
|
||||
<div *ngIf="addPeerStep === 1" style="animation: fadeIn 0.3s ease-in-out;">
|
||||
<h6 class="mb-4 text-primary">Step 1: General Info</h6>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText><i class="fas fa-tag"></i></span>
|
||||
<input cFormControl [(ngModel)]="peerForm.name" placeholder="Peer Name (e.g. CEO Laptop)" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText><i class="fas fa-align-left"></i></span>
|
||||
<textarea cFormControl [(ngModel)]="peerForm.description" placeholder="Optional description..."
|
||||
rows="2"></textarea>
|
||||
</c-input-group>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Addressing -->
|
||||
<div *ngIf="addPeerStep === 2" style="animation: fadeIn 0.3s ease-in-out;">
|
||||
<h6 class="mb-4 text-primary">Step 2: Addressing</h6>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>Assigned IP</span>
|
||||
<input cFormControl [(ngModel)]="peerForm.custom_ip" placeholder="Leave empty for Auto-assign" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3" *ngIf="editingPeer">
|
||||
<span cInputGroupText>Public Key</span>
|
||||
<input cFormControl [(ngModel)]="peerForm.pubkey" readonly />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>Keepalive</span>
|
||||
<input cFormControl type="number" [(ngModel)]="peerForm.persistent_keepalive" placeholder="25" />
|
||||
<span cInputGroupText>seconds</span>
|
||||
</c-input-group>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Routing -->
|
||||
<div *ngIf="addPeerStep === 3" style="animation: fadeIn 0.3s ease-in-out;">
|
||||
<h6 class="mb-4 text-primary">Step 3: Routing</h6>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText>NAT Mode</span>
|
||||
<select cSelect [(ngModel)]="peerForm.nat_mode">
|
||||
<option value="full">Full Tunnel (Route all traffic)</option>
|
||||
<option value="split">Split Tunnel (Route specific subnets)</option>
|
||||
<option value="off">Off (Direct Routing)</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
|
||||
<div *ngIf="peerForm.nat_mode === 'split'" class="mt-3">
|
||||
<h6>Split Tunnel Targets</h6>
|
||||
<div *ngFor="let target of peerForm.split_targets; let i = index; trackBy: trackByIndex" class="d-flex mb-2">
|
||||
<input cFormControl [(ngModel)]="peerForm.split_targets[i]" placeholder="e.g. 192.168.1.0/24"
|
||||
class="me-2" />
|
||||
<button cButton color="danger" variant="outline" (click)="removeSplitTarget(i)"><i
|
||||
class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
<button cButton color="info" size="sm" variant="outline" (click)="addSplitTarget()">
|
||||
<i class="fas fa-plus"></i> Add Subnet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Identity -->
|
||||
<div *ngIf="addPeerStep === 4" style="animation: fadeIn 0.3s ease-in-out;">
|
||||
<h6 class="mb-4 text-primary">Step 4: Identity & Integrations</h6>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="linkDeviceCheck" [(ngModel)]="peerForm.link_device">
|
||||
<label class="form-check-label" for="linkDeviceCheck">
|
||||
Link & Manage as MikroTik Device
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div *ngIf="peerForm.link_device" class="mt-3" style="animation: fadeIn 0.3s ease-in-out;">
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText><i class="fas fa-user"></i></span>
|
||||
<input cFormControl [(ngModel)]="peerForm.mt_user" placeholder="MikroTik Username" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText><i class="fas fa-lock"></i></span>
|
||||
<input cFormControl type="password" [(ngModel)]="peerForm.mt_pass" placeholder="MikroTik Password" />
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-3">
|
||||
<span cInputGroupText><i class="fas fa-network-wired"></i></span>
|
||||
<input cFormControl type="number" [(ngModel)]="peerForm.mt_port" placeholder="8728" />
|
||||
</c-input-group>
|
||||
|
||||
<div class="alert alert-info mt-3" style="font-size: 0.85rem;">
|
||||
<i class="fas fa-info-circle"></i> Once the peer connects and routes traffic (Online), the MikroWizard
|
||||
native scanner will automatically use these credentials in the background to discover and safely add the
|
||||
router into your Devices panel!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Summary -->
|
||||
<div *ngIf="addPeerStep === 5" style="animation: fadeIn 0.3s ease-in-out;">
|
||||
<h6 class="mb-4 text-primary">Step 5: Summary</h6>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"><strong>IP Assignment Preview:</strong> {{ peerForm.custom_ip || 'Auto-assigned'
|
||||
}}</li>
|
||||
<li class="list-group-item"><strong>Routing NAT Mode:</strong> <c-badge color="info">{{ peerForm.nat_mode |
|
||||
uppercase }}</c-badge></li>
|
||||
<li class="list-group-item" *ngIf="peerForm.nat_mode === 'split'"><strong>Split Targets:</strong> {{
|
||||
peerForm.split_targets.join(', ') || 'None' }}</li>
|
||||
<li class="list-group-item"><strong>MikroTik Integration:</strong> {{ peerForm.link_device ? 'Enabled' :
|
||||
'Disabled' }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="addPeerStep > 1" cButton color="secondary" (click)="addPeerStep = addPeerStep - 1">Back</button>
|
||||
<button *ngIf="addPeerStep < 5" cButton color="primary" (click)="addPeerStep = addPeerStep + 1">Next</button>
|
||||
<button *ngIf="addPeerStep === 5" cButton color="success" (click)="submitPeer()">
|
||||
<i class="fas fa-save"></i> {{ editingPeer ? 'Save Changes' : 'Create Peer' }}
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
|
||||
|
||||
<!-- Server Reset Confirm Modal -->
|
||||
<c-modal id="resetServerModal" [visible]="resetServerModalVisible" (visibleChange)="resetServerModalVisible = $event">
|
||||
<c-modal-header class="bg-warning text-dark">
|
||||
<h5 cModalTitle><i class="fas fa-exclamation-triangle"></i> Reset Server Counters</h5>
|
||||
<button cButtonClose (click)="resetServerModalVisible = false"></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
Are you sure you want to reset all data transfer counters for the VPN Server?
|
||||
<br><br>
|
||||
<small class="text-muted"><i class="fas fa-info-circle"></i> This will immediately set all Total Rx/Tx metrics to
|
||||
zero. This does not disconnect any peers.</small>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="secondary" (click)="resetServerModalVisible = false">Cancel</button>
|
||||
<button cButton color="warning" (click)="confirmResetServer()">Reset Counters</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Peer Reset Confirm Modal -->
|
||||
<c-modal id="resetPeerModal" [visible]="resetPeerModalVisible" (visibleChange)="resetPeerModalVisible = $event">
|
||||
<c-modal-header class="bg-warning text-dark">
|
||||
<h5 cModalTitle><i class="fas fa-exclamation-triangle"></i> Reset Peer Counters</h5>
|
||||
<button cButtonClose (click)="resetPeerModalVisible = false"></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body *ngIf="peerToReset">
|
||||
Are you sure you want to reset the traffic counters for peer: <strong>{{ peerToReset.name ||
|
||||
peerToReset.assigned_ip }}</strong>?
|
||||
<br><br>
|
||||
<small class="text-muted"><i class="fas fa-info-circle"></i> This clears their individual Rx/Tx metrics to zero
|
||||
and does not disconnect them.</small>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="secondary" (click)="resetPeerModalVisible = false">Cancel</button>
|
||||
<button cButton color="warning" (click)="confirmResetPeer()">Reset Counters</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
</div>
|
||||
<app-license-expired-overlay></app-license-expired-overlay>
|
||||
</div>
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
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);
|
||||
}
|
||||
}
|
||||
589
src/app/views/vpn/vpn.component.ts
Normal file
589
src/app/views/vpn/vpn.component.ts
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList } from '@angular/core';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { loginChecker } from '../../providers/login_checker';
|
||||
import { VpnService, VpnStatusResponse, VpnPeer, VpnServerConfig } from '../../providers/mikrowizard/vpn.service';
|
||||
import { Subscription, delay, of, repeat, switchMap, timer, catchError } from 'rxjs';
|
||||
import { Table } from 'primeng/table';
|
||||
import { ToasterComponent, ToasterPlacement } from "@coreui/angular";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: 'vpn.component.html',
|
||||
styleUrls: ['vpn.component.scss']
|
||||
})
|
||||
export class VpnComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("dt") table!: Table;
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
delay: 3000,
|
||||
position: 'fixed' as ToasterPlacement,
|
||||
fade: true,
|
||||
closeButton: true,
|
||||
};
|
||||
|
||||
public status: VpnStatusResponse | null = null;
|
||||
private pollingSubscription?: Subscription;
|
||||
private livePollingSubscription?: Subscription;
|
||||
|
||||
// Aggregate stats
|
||||
public totalRx: number = 0;
|
||||
public totalTx: number = 0;
|
||||
public liveSpeedRx: number = 0;
|
||||
public liveSpeedTx: number = 0;
|
||||
public isCommunicationError: boolean = false;
|
||||
|
||||
formatBytes(bytes: number, decimals: number = 2): string {
|
||||
if (!+bytes) return '0 B';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
public chartData: any = {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Rx Speed',
|
||||
backgroundColor: 'rgba(46, 184, 92, 0.1)',
|
||||
borderColor: '#2eb85c',
|
||||
pointBackgroundColor: '#2eb85c',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
data: []
|
||||
},
|
||||
{
|
||||
label: 'Tx Speed',
|
||||
backgroundColor: 'rgba(51, 153, 255, 0.1)',
|
||||
borderColor: '#3399ff',
|
||||
pointBackgroundColor: '#3399ff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
data: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
public chartOptions: any = {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += this.formatBytes(context.parsed.y) + '/s';
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { display: true },
|
||||
y: {
|
||||
display: true,
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (value: any) => {
|
||||
return this.formatBytes(value) + '/s';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: { tension: 0.4 },
|
||||
point: { radius: 0, hitRadius: 10, hoverRadius: 4 }
|
||||
}
|
||||
};
|
||||
|
||||
// Grid configs
|
||||
public source: Array<VpnPeer> = [];
|
||||
public loading: boolean = true;
|
||||
|
||||
// Modals state
|
||||
public addPeerModalVisible = false;
|
||||
public addPeerStep = 1;
|
||||
public peerForm: any = {
|
||||
pubkey: '',
|
||||
custom_ip: '',
|
||||
name: '',
|
||||
description: '',
|
||||
nat_mode: 'full',
|
||||
split_targets: [''], // Array of strings
|
||||
link_device: false,
|
||||
persistent_keepalive: 25,
|
||||
custom_interface: '',
|
||||
mt_user: '',
|
||||
mt_pass: '',
|
||||
mt_port: 8728
|
||||
};
|
||||
public editingPeer = false;
|
||||
|
||||
public configResult: { script?: string, qrBlobUrl?: SafeUrl } = {};
|
||||
public activePeerConfig: VpnPeer | null = null;
|
||||
|
||||
public serverConfigModalVisible = false;
|
||||
public serverConfig: Partial<VpnServerConfig> = {};
|
||||
|
||||
public deleteModalVisible = false;
|
||||
public peerToDelete: VpnPeer | null = null;
|
||||
|
||||
public toggleModalVisible = false;
|
||||
public peerToToggle: VpnPeer | null = null;
|
||||
|
||||
public resetPeerModalVisible = false;
|
||||
public peerToReset: VpnPeer | null = null;
|
||||
|
||||
public resetServerModalVisible = false;
|
||||
|
||||
applyFilterGlobal($event: any, stringVal: string) {
|
||||
this.table.filterGlobal(($event.target as HTMLInputElement).value, stringVal);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private login_checker: loginChecker,
|
||||
private router: Router,
|
||||
private vpnService: VpnService,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
if (!this.login_checker.isLoggedIn()) {
|
||||
setTimeout(() => this.router.navigate(["login"]), 100);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.startPolling();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.pollingSubscription) {
|
||||
this.pollingSubscription.unsubscribe();
|
||||
}
|
||||
if (this.livePollingSubscription) {
|
||||
this.livePollingSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
// Slow poll for heavy metadata (10s)
|
||||
this.pollingSubscription = timer(0, 10000).pipe(
|
||||
switchMap(() => this.vpnService.getStatus().pipe(
|
||||
catchError(err => {
|
||||
console.error("VPN Polling Error:", err);
|
||||
return of(null);
|
||||
})
|
||||
))
|
||||
).subscribe({
|
||||
next: (res) => {
|
||||
if (res) {
|
||||
this.status = res;
|
||||
this.isCommunicationError = res.status === 'failed' && (res.error?.includes('Connection refused') || false);
|
||||
|
||||
if (!this.isCommunicationError) {
|
||||
this.source = (res.peers || []).map(p => ({
|
||||
...p,
|
||||
_search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}`
|
||||
}));
|
||||
this.computeTotals();
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.loading = false;
|
||||
this.source = [];
|
||||
}
|
||||
} else {
|
||||
if (!this.status) {
|
||||
this.loading = true; // Show loading if we never got a successful status
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fast poll for live bandwidth (2s)
|
||||
this.livePollingSubscription = timer(2000, 2000).pipe(
|
||||
switchMap(() => this.vpnService.getLiveStatus().pipe(
|
||||
catchError(err => of(null))
|
||||
))
|
||||
).subscribe({
|
||||
next: (liveData) => {
|
||||
if (liveData) {
|
||||
// Update server top card speeds
|
||||
this.liveSpeedRx = liveData.server.rx_speed || 0;
|
||||
this.liveSpeedTx = liveData.server.tx_speed || 0;
|
||||
this.totalRx = liveData.server.rx_bytes;
|
||||
this.totalTx = liveData.server.tx_bytes;
|
||||
|
||||
// Surgically update existing peers without re-creating array
|
||||
if (this.source && this.source.length > 0 && Array.isArray(liveData.peers)) {
|
||||
for (let i = 0; i < this.source.length; i++) {
|
||||
const peer = this.source[i];
|
||||
const pubkey = peer.public_key;
|
||||
|
||||
const livePeer = liveData.peers.find(p => p.public_key === pubkey);
|
||||
|
||||
if (livePeer && livePeer.stats && peer.stats) {
|
||||
// Instead of making a new object (which triggers grid redraw), mutate the stats deeply
|
||||
peer.stats.rx_bytes = livePeer.stats.rx_bytes;
|
||||
peer.stats.tx_bytes = livePeer.stats.tx_bytes;
|
||||
peer.stats.rx_speed = livePeer.stats.rx_speed;
|
||||
peer.stats.tx_speed = livePeer.stats.tx_speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.vpnService.getStatus().subscribe({
|
||||
next: (res) => {
|
||||
if (res) {
|
||||
this.status = res;
|
||||
this.isCommunicationError = res.status === 'failed' && (res.error?.includes('Connection refused') || false);
|
||||
|
||||
if (!this.isCommunicationError) {
|
||||
this.source = (res.peers || []).map(p => ({
|
||||
...p,
|
||||
_search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}`
|
||||
}));
|
||||
this.computeTotals();
|
||||
} else {
|
||||
this.source = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (err) => console.error("Error refreshing data:", err)
|
||||
});
|
||||
}
|
||||
|
||||
computeTotals() {
|
||||
this.totalRx = 0;
|
||||
this.totalTx = 0;
|
||||
for (const p of this.source) {
|
||||
if (p.stats) {
|
||||
this.totalRx += p.stats.rx_bytes / 1048576; // To MB
|
||||
this.totalTx += p.stats.tx_bytes / 1048576; // To MB
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const timeLabel = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
this.chartData.labels.push(timeLabel);
|
||||
this.chartData.datasets[0].data.push(this.liveSpeedRx);
|
||||
this.chartData.datasets[1].data.push(this.liveSpeedTx);
|
||||
|
||||
// Keep last 30 intervals (2.5 minutes of history)
|
||||
if (this.chartData.labels.length > 30) {
|
||||
this.chartData.labels.shift();
|
||||
this.chartData.datasets[0].data.shift();
|
||||
this.chartData.datasets[1].data.shift();
|
||||
}
|
||||
|
||||
// Trigger change detection for chart
|
||||
this.chartData = { ...this.chartData };
|
||||
}
|
||||
|
||||
show_toast(title: string, body: string, color: string) {
|
||||
const props = { ...this.toasterForm, color, title, body };
|
||||
if (this.viewChildren && this.viewChildren.first) {
|
||||
const componentRef = this.viewChildren.first.addToast(
|
||||
AppToastComponent,
|
||||
props,
|
||||
{}
|
||||
);
|
||||
if (componentRef) {
|
||||
componentRef.instance["closeButton"] = props.closeButton;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Modals & Actions ---
|
||||
|
||||
openAddPeerModal() {
|
||||
this.addPeerModalVisible = true;
|
||||
this.addPeerStep = 1;
|
||||
this.editingPeer = false;
|
||||
this.peerForm = {
|
||||
pubkey: '', custom_ip: '', name: '', description: '', nat_mode: 'full', split_targets: [''], link_device: false, persistent_keepalive: 25, custom_interface: '',
|
||||
mt_user: '', mt_pass: '', mt_port: 8728
|
||||
};
|
||||
}
|
||||
|
||||
addSplitTarget() {
|
||||
this.peerForm.split_targets.push('');
|
||||
}
|
||||
|
||||
removeSplitTarget(index: number) {
|
||||
this.peerForm.split_targets.splice(index, 1);
|
||||
}
|
||||
|
||||
trackByIndex(index: number, obj: any): any {
|
||||
return index;
|
||||
}
|
||||
|
||||
submitPeer() {
|
||||
// Filter empty splits
|
||||
const payload = { ...this.peerForm };
|
||||
payload.split_targets = payload.split_targets.filter((s: string) => s.trim() !== '');
|
||||
|
||||
// Scrub credentials if the user unchecked "Link & Manage as MikroTik Device"
|
||||
if (!payload.link_device) {
|
||||
payload.mt_user = null;
|
||||
payload.mt_pass = null;
|
||||
payload.mt_port = null;
|
||||
}
|
||||
|
||||
const isLinkDevice = payload.link_device;
|
||||
|
||||
// Remove the deprecated 'link_device' flag from the API payload entirely
|
||||
delete payload.link_device;
|
||||
|
||||
if (this.editingPeer) {
|
||||
this.vpnService.editPeer(payload).subscribe({
|
||||
next: (res) => {
|
||||
this.show_toast("Success", "Peer updated successfully", "success");
|
||||
this.addPeerModalVisible = false;
|
||||
this.refreshData();
|
||||
},
|
||||
error: (err) => this.show_toast("Error", err.error?.message || "Failed to update peer", "danger")
|
||||
});
|
||||
} else {
|
||||
this.vpnService.addPeer(payload).subscribe({
|
||||
next: (res) => {
|
||||
this.show_toast("Success", "Peer created successfully", "success");
|
||||
this.addPeerModalVisible = false;
|
||||
this.refreshData();
|
||||
// Auto-open appropriate config wizard
|
||||
if (isLinkDevice) {
|
||||
this.openScriptModal(res.peer);
|
||||
} else {
|
||||
this.openQrModal(res.peer);
|
||||
}
|
||||
},
|
||||
error: (err) => this.show_toast("Error", err.error?.message || "Failed to add peer", "danger")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
promptToggleEnabled(item: VpnPeer) {
|
||||
this.peerToToggle = item;
|
||||
this.toggleModalVisible = true;
|
||||
}
|
||||
|
||||
confirmToggle() {
|
||||
if (!this.peerToToggle) return;
|
||||
const item = this.peerToToggle;
|
||||
const newState = !item.is_enabled;
|
||||
|
||||
this.vpnService.togglePeer(item.public_key, newState).subscribe({
|
||||
next: () => {
|
||||
this.show_toast("Success", `Peer ${newState ? 'enabled' : 'disabled'}`, "success");
|
||||
item.is_enabled = newState;
|
||||
this.source = [...this.source]; // Force grid update
|
||||
this.refreshData();
|
||||
this.toggleModalVisible = false;
|
||||
this.peerToToggle = null;
|
||||
},
|
||||
error: (err) => {
|
||||
this.show_toast("Error", err.error?.message || "Failed to toggle peer", "danger");
|
||||
this.toggleModalVisible = false;
|
||||
this.peerToToggle = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openEditModal(item: VpnPeer) {
|
||||
this.editingPeer = true;
|
||||
this.addPeerModalVisible = true;
|
||||
this.addPeerStep = 1;
|
||||
this.peerForm = {
|
||||
pubkey: item.public_key,
|
||||
custom_ip: item.assigned_ip,
|
||||
name: item.name || '',
|
||||
description: item.description || '',
|
||||
nat_mode: item.nat_mode,
|
||||
split_targets: [...item.split_targets],
|
||||
link_device: !!item.linked_device_id || !!item.mt_user,
|
||||
persistent_keepalive: item.persistent_keepalive,
|
||||
custom_interface: item.custom_interface || '',
|
||||
mt_user: item.mt_user || '',
|
||||
mt_pass: item.mt_pass || '',
|
||||
mt_port: item.mt_port || 8728
|
||||
};
|
||||
if (this.peerForm.split_targets.length === 0) this.peerForm.split_targets.push('');
|
||||
}
|
||||
|
||||
scanDevice(item: VpnPeer) {
|
||||
this.vpnService.scanLinkedDevice(item.public_key).subscribe({
|
||||
next: (res) => this.show_toast("Success", res.message || "Manual scan initiated asynchronously", "success"),
|
||||
error: (err) => this.show_toast("Error", err.error?.message || "Scan failed", "danger")
|
||||
});
|
||||
}
|
||||
|
||||
public scriptModalVisible = false;
|
||||
public qrModalVisible = false;
|
||||
|
||||
// Fetch and show script
|
||||
openScriptModal(item: VpnPeer) {
|
||||
this.activePeerConfig = item;
|
||||
this.configResult = {};
|
||||
this.scriptModalVisible = true;
|
||||
this.qrModalVisible = false; // Auto-close QR if open
|
||||
this.vpnService.getPeerMikrotikScript(item.public_key).subscribe({
|
||||
next: (res) => {
|
||||
const rawScript = res.script || '';
|
||||
// Ensure double-escaped newlines and carriage returns are aggressively parsed to real newlines
|
||||
this.configResult.script = rawScript.split('\\n').join('\n').replace(/\r\n/g, '\n');
|
||||
this.scriptModalVisible = true;
|
||||
},
|
||||
error: (err) => this.show_toast("Error", "Failed to fetch Mikrotik script", "danger")
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch and show QR
|
||||
openQrModal(item: VpnPeer) {
|
||||
this.activePeerConfig = item;
|
||||
this.configResult = {};
|
||||
this.scriptModalVisible = false; // Auto-close Script if open
|
||||
this.vpnService.getPeerQrCode(item.public_key).subscribe({
|
||||
next: (blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
this.configResult.qrBlobUrl = this.sanitizer.bypassSecurityTrustUrl(url);
|
||||
this.qrModalVisible = true;
|
||||
},
|
||||
error: (err) => this.show_toast("Error", "Failed to fetch QR code", "danger")
|
||||
});
|
||||
}
|
||||
|
||||
// Download config directly
|
||||
downloadPeerConfigDirect(item: VpnPeer) {
|
||||
this.vpnService.getPeerConfig(item.public_key).subscribe({
|
||||
next: (res) => {
|
||||
const blob = new Blob([res.config], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${item.assigned_ip || 'peer'}.conf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
},
|
||||
error: (err) => this.show_toast("Error", "Failed to fetch config", "danger")
|
||||
});
|
||||
}
|
||||
|
||||
copyScript() {
|
||||
if (this.configResult.script) {
|
||||
navigator.clipboard.writeText(this.configResult.script).then(() => {
|
||||
this.show_toast("Success", "Copied to clipboard", "success");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete(item: VpnPeer) {
|
||||
this.peerToDelete = item;
|
||||
this.deleteModalVisible = true;
|
||||
}
|
||||
|
||||
executeDelete() {
|
||||
if (this.peerToDelete) {
|
||||
this.vpnService.deletePeer(this.peerToDelete.public_key).subscribe({
|
||||
next: () => {
|
||||
this.show_toast("Success", "Peer deleted permanently", "success");
|
||||
this.deleteModalVisible = false;
|
||||
this.refreshData();
|
||||
},
|
||||
error: (err) => this.show_toast("Error", err.error?.message || "Delete failed", "danger")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openServerConfigModal() {
|
||||
this.serverConfigModalVisible = true;
|
||||
this.vpnService.getSystemConfig().subscribe({
|
||||
next: (res) => this.serverConfig = res.config || {},
|
||||
error: (err) => this.show_toast("Error", "Failed to fetch config", "danger")
|
||||
});
|
||||
}
|
||||
|
||||
saveServerConfig() {
|
||||
this.vpnService.updateSystemConfig(this.serverConfig).subscribe({
|
||||
next: () => {
|
||||
this.show_toast("Success", "Server config updated", "success");
|
||||
this.serverConfigModalVisible = false;
|
||||
this.refreshData();
|
||||
},
|
||||
error: (err) => this.show_toast("Error", err.error?.message || "Update failed", "danger")
|
||||
});
|
||||
}
|
||||
|
||||
// === COUNTER RESETS ===
|
||||
promptResetServer() {
|
||||
this.resetServerModalVisible = true;
|
||||
}
|
||||
|
||||
confirmResetServer() {
|
||||
this.vpnService.resetServerCounters().subscribe({
|
||||
next: () => {
|
||||
this.resetServerModalVisible = false;
|
||||
this.show_toast('Success', 'Global server traffic counters have been reset.', 'success');
|
||||
this.totalRx = 0;
|
||||
this.totalTx = 0;
|
||||
// Force chart data flush
|
||||
this.chartData = {
|
||||
...this.chartData,
|
||||
labels: [],
|
||||
datasets: [
|
||||
{ ...this.chartData.datasets[0], data: [] },
|
||||
{ ...this.chartData.datasets[1], data: [] }
|
||||
]
|
||||
};
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
this.resetServerModalVisible = false;
|
||||
this.show_toast('Error', 'Failed to reset server counters.', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
promptResetPeer(peer: VpnPeer) {
|
||||
this.peerToReset = peer;
|
||||
this.resetPeerModalVisible = true;
|
||||
}
|
||||
|
||||
confirmResetPeer() {
|
||||
if (!this.peerToReset || !this.peerToReset.public_key) return;
|
||||
this.vpnService.resetPeerCounters(this.peerToReset.public_key).subscribe({
|
||||
next: () => {
|
||||
this.show_toast('Success', `Counters reset for ${this.peerToReset?.name || this.peerToReset?.assigned_ip}`, 'success');
|
||||
if (this.peerToReset && this.peerToReset.stats) {
|
||||
this.peerToReset.stats.rx_bytes = 0;
|
||||
this.peerToReset.stats.tx_bytes = 0;
|
||||
this.peerToReset.stats.rx_speed = 0;
|
||||
this.peerToReset.stats.tx_speed = 0;
|
||||
}
|
||||
this.resetPeerModalVisible = false;
|
||||
this.peerToReset = null;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
this.resetPeerModalVisible = false;
|
||||
this.show_toast('Error', `Failed to reset counters for peer.`, 'danger');
|
||||
this.peerToReset = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
77
src/app/views/vpn/vpn.module.ts
Normal file
77
src/app/views/vpn/vpn.module.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ChartjsModule } from '@coreui/angular-chartjs';
|
||||
import { HighlightJsModule } from 'ngx-highlight-js';
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { SharedModule as AppSharedModule } from "../../shared/shared.module";
|
||||
|
||||
import {
|
||||
AvatarModule,
|
||||
ButtonGroupModule,
|
||||
ButtonModule,
|
||||
CardModule,
|
||||
FormModule,
|
||||
GridModule as CoreUIGridModule,
|
||||
NavModule,
|
||||
ProgressModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
ModalModule,
|
||||
DropdownModule,
|
||||
SharedModule,
|
||||
ListGroupModule,
|
||||
BadgeModule,
|
||||
TooltipModule,
|
||||
ToastModule
|
||||
} from '@coreui/angular';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
|
||||
import { loginChecker } from '../../providers/login_checker';
|
||||
import { VpnService, VpnStatusResponse, VpnPeer, VpnServerConfig } from '../../providers/mikrowizard/vpn.service';
|
||||
import { IconModule } from '@coreui/icons-angular';
|
||||
|
||||
import { VpnRoutingModule } from './vpn-routing.module';
|
||||
import { VpnComponent } from './vpn.component';
|
||||
import { TableModule as PTableModule } from 'primeng/table';
|
||||
import { TooltipModule as PTooltipModule } from 'primeng/tooltip';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
VpnRoutingModule,
|
||||
CardModule,
|
||||
NavModule,
|
||||
IconModule,
|
||||
TabsModule,
|
||||
CommonModule,
|
||||
PTableModule,
|
||||
PTooltipModule,
|
||||
InputTextModule,
|
||||
ProgressModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonModule,
|
||||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
ChartjsModule,
|
||||
AvatarModule,
|
||||
TableModule,
|
||||
ModalModule,
|
||||
DropdownModule,
|
||||
SharedModule,
|
||||
ListGroupModule,
|
||||
BadgeModule,
|
||||
TooltipModule,
|
||||
FormsModule,
|
||||
ToastModule,
|
||||
CoreUIGridModule,
|
||||
MatMenuModule,
|
||||
HighlightJsModule,
|
||||
ClipboardModule,
|
||||
AppSharedModule
|
||||
],
|
||||
declarations: [VpnComponent]
|
||||
})
|
||||
export class VpnModule {
|
||||
}
|
||||
|
|
@ -1,116 +1,165 @@
|
|||
// Here you can add other styles
|
||||
|
||||
|
||||
ngx-super-select.styled>div > span{
|
||||
background:var(--cui-input-group-addon-bg, #d8dbe0);
|
||||
height:100%;
|
||||
padding:8px;
|
||||
color:var(--cui-input-group-addon-color, rgba(44, 56, 74, 0.95))!important;
|
||||
ngx-super-select.styled>div>span {
|
||||
background: var(--cui-input-group-addon-bg, #d8dbe0);
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
color: var(--cui-input-group-addon-color, rgba(44, 56, 74, 0.95)) !important;
|
||||
}
|
||||
|
||||
ngx-super-select.styled>div {
|
||||
padding:0!important;
|
||||
width:100%;
|
||||
padding: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
ngx-super-select.styled{
|
||||
width:100%;
|
||||
|
||||
ngx-super-select.styled {
|
||||
width: 100%;
|
||||
}
|
||||
ngx-super-select.styled.hiden .select-selected-items:before{
|
||||
content:"click for select"
|
||||
|
||||
ngx-super-select.styled.hiden .select-selected-items:before {
|
||||
content: "click for select"
|
||||
}
|
||||
ngx-super-select.styled.hiden .select-selected-items > *{
|
||||
display:none!important;
|
||||
|
||||
ngx-super-select.styled.hiden .select-selected-items>* {
|
||||
display: none !important;
|
||||
}
|
||||
ngx-super-select.styled .selection-counter:before{
|
||||
content:"Selected Count : "
|
||||
|
||||
ngx-super-select.styled .selection-counter:before {
|
||||
content: "Selected Count : "
|
||||
}
|
||||
ngx-super-select.styled .selection-counter{
|
||||
color:#000!important;
|
||||
background:#aaaaaa26!important;
|
||||
margin:0!important;
|
||||
|
||||
ngx-super-select.styled .selection-counter {
|
||||
color: #000 !important;
|
||||
background: #aaaaaa26 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.cdk-overlay-container {
|
||||
z-index: 10000;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.mdc-text-field--no-label:not(.mdc-text-field--outlined):not(.mdc-text-field--textarea) .mat-mdc-form-field-infix {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.mat-mdc-form-field-infix {
|
||||
min-height: 34px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.mdc-text-field--filled:not(.mdc-text-field--disabled){
|
||||
background: transparent!important;
|
||||
.mat-mdc-form-field-infix {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
/* Add application styles & imports to this file! */
|
||||
|
||||
pre {
|
||||
display: flex!important;
|
||||
margin-top: 0!important;
|
||||
margin-bottom: 0!important;
|
||||
word-wrap: break-word!important;
|
||||
}
|
||||
|
||||
code {
|
||||
flex: 1!important;
|
||||
line-height: 1.8em!important;
|
||||
font-size: 14px!important;
|
||||
min-height: 100%!important;
|
||||
padding: 1em 1.2em!important;
|
||||
overflow-x: unset!important;
|
||||
overflow-y: unset!important;
|
||||
}
|
||||
|
||||
.hljs.hljs-line-numbers {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
pre .hljs {
|
||||
border: none!important;
|
||||
transition: border ease 1s!important;
|
||||
}
|
||||
|
||||
.hljs-ln {
|
||||
tr {
|
||||
&:first-child td {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
display: flex !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
word-wrap: break-word !important;
|
||||
}
|
||||
|
||||
code {
|
||||
flex: 1 !important;
|
||||
line-height: 1.8em !important;
|
||||
font-size: 14px !important;
|
||||
min-height: 100% !important;
|
||||
padding: 1em 1.2em !important;
|
||||
overflow-x: unset !important;
|
||||
overflow-y: unset !important;
|
||||
}
|
||||
|
||||
.hljs.hljs-line-numbers {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
pre .hljs {
|
||||
border: none !important;
|
||||
transition: border ease 1s !important;
|
||||
}
|
||||
|
||||
.hljs-ln {
|
||||
tr {
|
||||
&:first-child td {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
.row-highlighted{
|
||||
z-index: 0!important;
|
||||
}
|
||||
.mat-mdc-menu-panel,.mat-mdc-menu-content{
|
||||
background: transparent!important;
|
||||
padding: 0!important;
|
||||
}
|
||||
/* for block of numbers */
|
||||
td.hljs-ln-numbers {
|
||||
position: sticky!important;
|
||||
left: 0!important;
|
||||
user-select: none!important;
|
||||
text-align: center!important;
|
||||
color: #cccccc6b!important;
|
||||
border-right: 1px solid #cccccc1c!important;
|
||||
vertical-align: top!important;
|
||||
padding-right: 10px !important;
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
/* for block of code */
|
||||
td.hljs-ln-code {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.gui-structure, .gui-structure *{
|
||||
font-size: inherit!important;
|
||||
}
|
||||
.row-highlighted {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
.mat-mdc-menu-panel,
|
||||
.mat-mdc-menu-content {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* for block of numbers */
|
||||
td.hljs-ln-numbers {
|
||||
position: sticky !important;
|
||||
left: 0 !important;
|
||||
user-select: none !important;
|
||||
text-align: center !important;
|
||||
color: #cccccc6b !important;
|
||||
border-right: 1px solid #cccccc1c !important;
|
||||
vertical-align: top !important;
|
||||
padding-right: 10px !important;
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
/* for block of code */
|
||||
td.hljs-ln-code {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
|
||||
.gui-structure,
|
||||
.gui-structure * {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
.p-datatable-table-container thead th:first-of-type {
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
|
||||
.p-datatable-table-container thead th:last-of-type {
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.p-datatable-table-container thead th {
|
||||
padding: 0.30rem 1rem !important;
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.table-search-input input {
|
||||
border-top: none;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-bottom: 1px !important solid;
|
||||
border-radius: 0;
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
.table-search-input {
|
||||
padding-top: 15px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
p-table {
|
||||
width: 100%
|
||||
}
|
||||
|
|
@ -25,4 +25,5 @@ $enable-rtl: true;
|
|||
// animations
|
||||
@import "animations";
|
||||
|
||||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||
@import "primeicons/primeicons.css";
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": [
|
||||
"@angular/localize"
|
||||
"@angular/localize",
|
||||
"hammerjs"
|
||||
],
|
||||
"paths": {
|
||||
"@docs-components/*": ["./src/components/*"]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
|
|
@ -23,7 +23,11 @@
|
|||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@primeuix/themes/*": ["node_modules/@primeuix/themes/dist/*"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue