import { Component, Inject, OnInit, OnDestroy, AfterViewInit, ElementRef, ViewChild, NgZone, ViewEncapsulation } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Subscription } from 'rxjs'; import { P3xrAccordionComponent } from '../../components/p3xr-accordion.component'; import { P3xrButtonComponent } from '../../components/p3xr-button.component'; import { I18nService } from '../../services/i18n.service'; import { MonitoringDataService, PubsubEntry } from '../monitoring/monitoring-data.service'; import { RedisStateService } from '../../services/redis-state.service'; @Component({ selector: 'p3xr-pubsub', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, P3xrAccordionComponent, P3xrButtonComponent], templateUrl: './pubsub.component.html', encapsulation: ViewEncapsulation.None, styles: [` p3xr-pubsub { display: block; color: var(--mat-app-text-color, inherit); } .p3xr-pubsub-output { font-family: 'Roboto Mono', monospace; font-size: 13px; overflow-y: auto; word-break: break-all; white-space: normal; } .p3xr-pubsub-entry { padding: 6px 16px; word-break: break-all; white-space: normal; } .p3xr-pubsub-entry-odd { background-color: var(--p3xr-list-odd-bg); } .p3xr-pubsub-pattern { padding: 8px 16px; } `], }) export class PubsubComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('pubsubOutput') pubsubOutputRef?: ElementRef; strings; get pubsubPattern(): string { return this.data.pubsubPattern; } set pubsubPattern(v: string) { this.data.pubsubPattern = v; } private readonly maxDomEntries = 66; private entryIndex = 0; private sub?: Subscription; private resizeFn: (() => void) | null = null; constructor( @Inject(I18nService) private readonly i18n: I18nService, @Inject(MonitoringDataService) private readonly data: MonitoringDataService, @Inject(RedisStateService) private readonly state: RedisStateService, @Inject(NgZone) private readonly ngZone: NgZone, ) { this.strings = this.i18n.strings; } ngOnInit(): void { setTimeout(() => { this.renderExistingEntries(); this.sub = this.data.pubsubEntry$.subscribe(entry => this.renderEntry(entry)); }); } ngAfterViewInit(): void { document.body.classList.add('p3xr-no-main-scroll'); this.ngZone.runOutsideAngular(() => { this.resizeFn = () => this.recalcHeight(); window.addEventListener('resize', this.resizeFn); setTimeout(() => { this.recalcHeight(); const el = this.pubsubOutputRef?.nativeElement; if (el) el.scrollTop = el.scrollHeight; }, 50); }); } ngOnDestroy(): void { document.body.classList.remove('p3xr-no-main-scroll'); this.sub?.unsubscribe(); if (this.resizeFn) window.removeEventListener('resize', this.resizeFn); } async restartPubSub(): Promise { await this.data.restartPubSub(); } clearPubSub(): void { this.data.clearPubSub(); this.entryIndex = 0; if (this.pubsubOutputRef?.nativeElement) { this.pubsubOutputRef.nativeElement.innerHTML = ''; } } exportPubSub(): void { const connName = this.state.connection()?.name || 'redis'; const lines = this.data.pubsubEntries.map(e => `${e.fullTimestamp} ${e.channel} ${e.message}`); this.downloadText(lines.join('\n'), `${connName}-pubsub-export.txt`); } private renderExistingEntries(): void { const el = this.pubsubOutputRef?.nativeElement; if (!el) return; const entries = this.data.pubsubEntries; const start = Math.max(0, entries.length - this.maxDomEntries); this.entryIndex = start; for (let i = start; i < entries.length; i++) { this.renderEntry(entries[i]); } el.scrollTop = el.scrollHeight; } private renderEntry(entry: PubsubEntry): void { const el = this.pubsubOutputRef?.nativeElement; if (!el) return; const odd = this.entryIndex++ % 2 === 1 ? ' p3xr-pubsub-entry-odd' : ''; el.insertAdjacentHTML('beforeend', `
${this.escapeHtml(entry.displayTime)} ${this.escapeHtml(entry.channel)} ${this.escapeHtml(entry.message)}
`); while (el.children.length > this.maxDomEntries) { el.removeChild(el.firstChild!); } el.scrollTop = el.scrollHeight; } private recalcHeight(): void { const el = this.pubsubOutputRef?.nativeElement; if (!el) return; const rect = el.getBoundingClientRect(); const footerHeight = document.getElementById('p3xr-layout-footer-container')?.offsetHeight || 48; const available = window.innerHeight - rect.top - footerHeight - 8; el.style.height = Math.max(available, 100) + 'px'; } private escapeHtml(str: string): string { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } private downloadText(content: string, filename: string): void { const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } }