import { Component, DestroyRef, computed, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatTableModule } from '@angular/material/table'; import { FEATURES } from '../core/models'; import { PortalApiService } from '../core/portal-api.service'; import { RealtimeService } from '../core/realtime.service'; import { SessionService } from '../core/session.service'; @Component({ selector: 'app-feature-page', imports: [MatButtonModule, MatCardModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTableModule], template: `

{{ feature().title }}

Manage {{ feature().title.toLowerCase() }}

Filtersearch @if (feature().path === 'syslogs' && session.hasPermission('PURGE_LOGS')) { } @if (canSave()) { }
@if (loading()) { } @else if (error()) { {{ error() }} } @else if (rows().length === 0) { No records found. } @else {
@for (column of dataColumns(); track column) { } @if (hasActions()) { }
{{ label(column) }}{{ display(row[column]) }}Actions @if (canSave()) { } @if (feature().path === 'scripts' && (session.hasPermission('VIEW_SCRIPT_SCHED_STATUS') || session.hasPermission('DELETE_SCRIPT_SCHED_STATUS'))) { } @if (canExecute()) { } @if (canDelete()) { }
}`, styles: [`header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}h1{margin:0}p{color:#607d8b}.toolbar{display:flex;gap:.5rem}.table-container{overflow:auto}table{width:100%}.error{color:#b00020;padding:1rem}.empty{padding:2rem;text-align:center}`] }) export class FeaturePageComponent { private readonly route = inject(ActivatedRoute); private readonly api = inject(PortalApiService); private readonly realtime = inject(RealtimeService); readonly session = inject(SessionService); private readonly dialog = inject(MatDialog); private readonly destroyRef = inject(DestroyRef); readonly loading = signal(true); readonly error = signal(''); readonly rows = signal[]>([]); readonly filter = signal(''); readonly displayedRows = computed(() => { const term=this.filter().trim().toLowerCase(); return term ? this.rows().filter(row=>Object.values(row).some(value=>String(value??'').toLowerCase().includes(term))) : this.rows(); }); readonly feature = computed(() => FEATURES.find(feature => feature.path === this.route.snapshot.data['feature'])!); readonly dataColumns = computed(() => Object.keys(this.rows()[0] ?? {}).filter(key => typeof this.rows()[0]?.[key] !== 'object').slice(0, 8)); readonly columns = computed(() => this.hasActions() ? [...this.dataColumns(), 'actions'] : this.dataColumns()); constructor() { this.load(); this.realtime.triggers.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.load()); } hasActions(): boolean { return this.canSave() || this.canDelete() || this.canExecute() || (this.feature().path === 'scripts' && (this.session.hasPermission('VIEW_SCRIPT_SCHED_STATUS') || this.session.hasPermission('DELETE_SCRIPT_SCHED_STATUS'))); } canSave(): boolean { return !!this.feature().editor && !!this.feature().saveEndpoint && this.session.hasPermission(this.feature().savePermission!); } canDelete(): boolean { return !!this.feature().deleteEndpoint && this.session.hasPermission(this.feature().deletePermission!); } canExecute(): boolean { return !!this.feature().executeEndpoint && this.session.hasPermission(this.feature().executePermission!); } load(): void { this.loading.set(true); this.error.set(''); this.api.request(this.feature().loadEndpoint).subscribe({ next: data => { this.rows.set(Array.isArray(data) ? data : (data ? [data as Record] : [])); this.loading.set(false); }, error: error => { this.error.set(error.message); this.loading.set(false); } }); } async edit(row?: Record): Promise { const config = { data: { feature: this.feature(), row }, disableClose: true, width: 'min(52rem, 95vw)' }; if (this.feature().editor === 'environment') { const { EnvironmentDialogComponent } = await import('./environment-dialog'); this.dialog.open(EnvironmentDialogComponent, config).afterClosed().subscribe(saved => { if (saved) this.load(); }); } else if (this.feature().editor === 'permission') { const { PermissionDialogComponent } = await import('./permission-dialog'); this.dialog.open(PermissionDialogComponent, config).afterClosed().subscribe(saved => { if (saved) this.load(); }); } else if (this.feature().editor === 'script') { const { ScriptDialogComponent } = await import('./script-dialog'); this.dialog.open(ScriptDialogComponent, config).afterClosed().subscribe(saved => { if (saved) this.load(); }); } else { const { EntityDialogComponent } = await import('./entity-dialog'); this.dialog.open(EntityDialogComponent, config).afterClosed().subscribe(saved => { if (saved) this.load(); }); } } remove(row: Record): void { if (confirm(`Delete this ${this.feature().title} record?`)) this.api.request(this.feature().deleteEndpoint!, this.feature().editor === 'environment' ? [row] : row).subscribe({ next: () => this.load(), error: error => this.error.set(error.message) }); } execute(row: Record): void { const verb=this.feature().path==='scripts'?'run':'pull';if(!confirm(`Are you sure you want to ${verb} ${String(row['name']??'this item')}?`))return;this.api.request(this.feature().executeEndpoint!, row).subscribe({ next: () => this.load(), error: error => this.error.set(error.message) }); } async purgeLogs(): Promise { const {PurgeLogsDialogComponent}=await import('./purge-logs-dialog');this.dialog.open(PurgeLogsDialogComponent,{disableClose:true}).afterClosed().subscribe(count=>{if(count!==undefined)this.load();}); } async openScriptStatus(row:Record):Promise{const {ScriptStatusDialogComponent}=await import('./script-status-dialog');this.dialog.open(ScriptStatusDialogComponent,{data:{script:row},width:'min(70rem,96vw)'}).afterClosed().subscribe(()=>this.load());} label(value: string): string { return value.replace(/([A-Z])/g, ' $1').replace(/^./, character => character.toUpperCase()); } display(value: unknown): string { return value == null ? '' : String(value); } }