/** * PubSub page — exact port of Angular pubsub.component. * Uses direct DOM manipulation for fast entry rendering. */ import { useEffect, useRef, useCallback } from 'react' import { Box, TextField, useTheme } from '@mui/material' import { Backspace, Download } from '@mui/icons-material' import { useI18nStore } from '../../stores/i18n.store' import { useRedisStateStore } from '../../stores/redis-state.store' import { useMonitoringDataStore, clearPubSub, onPubsubEntry, PubsubEntry, } from '../../stores/monitoring-data.store' import P3xrAccordion from '../../components/P3xrAccordion' import P3xrButton from '../../components/P3xrButton' const MAX_DOM_ENTRIES = 66 const TOOLBAR_HEIGHT = 48 function escapeHtml(str: string): string { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } function 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) } export default function PubSubPage() { const strings = useI18nStore(s => s.strings) const connection = useRedisStateStore(s => s.connection) const pubsubPattern = useMonitoringDataStore(s => s.pubsubPattern) const muiTheme = useTheme() const isDark = muiTheme.palette.mode === 'dark' const outputRef = useRef(null) const entryIndexRef = useRef(0) const oddBg = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)' const renderEntry = useCallback((entry: PubsubEntry) => { const el = outputRef.current if (!el) return const odd = entryIndexRef.current++ % 2 === 1 const div = document.createElement('div') div.style.padding = '6px 16px' div.style.wordBreak = 'break-all' div.style.whiteSpace = 'normal' if (odd) div.style.backgroundColor = oddBg div.innerHTML = `${escapeHtml(entry.displayTime)} ${escapeHtml(entry.channel)} ${escapeHtml(entry.message)}` el.appendChild(div) while (el.children.length > MAX_DOM_ENTRIES) el.removeChild(el.firstChild!) el.scrollTop = el.scrollHeight }, [oddBg]) const recalcHeight = useCallback(() => { const el = outputRef.current if (!el) return const rect = el.getBoundingClientRect() const available = window.innerHeight - rect.top - TOOLBAR_HEIGHT - 8 el.style.height = Math.max(available, 100) + 'px' }, []) useEffect(() => { document.body.classList.add('p3xr-no-main-scroll') const entries = useMonitoringDataStore.getState().pubsubEntries const start = Math.max(0, entries.length - MAX_DOM_ENTRIES) entryIndexRef.current = start for (let i = start; i < entries.length; i++) renderEntry(entries[i]) const el = outputRef.current if (el) el.scrollTop = el.scrollHeight const unsub = onPubsubEntry(renderEntry) const onResize = () => recalcHeight() window.addEventListener('resize', onResize) setTimeout(recalcHeight, 50) return () => { document.body.classList.remove('p3xr-no-main-scroll') unsub() window.removeEventListener('resize', onResize) } }, [renderEntry, recalcHeight]) const handleClear = useCallback(() => { clearPubSub() entryIndexRef.current = 0 if (outputRef.current) outputRef.current.innerHTML = '' }, []) const handleExport = useCallback(() => { const connName = connection?.name || 'redis' const entries = useMonitoringDataStore.getState().pubsubEntries const lines = entries.map(e => `${e.fullTimestamp} ${e.channel} ${e.message}`) downloadText(lines.join('\n'), `${connName}-pubsub-export.txt`) }, [connection]) const handlePatternChange = (v: string) => { useMonitoringDataStore.setState({ pubsubPattern: v }) } return ( } label={strings?.intention?.clear || 'Clear'} color="inherit" onClick={(e) => { e.stopPropagation(); handleClear() }} /> } label={strings?.intention?.export || 'Export'} color="inherit" onClick={(e) => { e.stopPropagation(); handleExport() }} /> } > handlePatternChange(e.target.value)} /> ) }