import React, { useMemo, useState } from 'react'; import { Copy, Clock, Image as ImageIcon, LineChart, Code, History, GitCompare, Braces } from 'lucide-react'; import { ChartsDock } from './ChartsDock'; import { MQTTMessage } from '../types'; interface TopicDetailsProps { topic: string; lastMessage: MQTTMessage | null; previousMessage: MQTTMessage | null; maxPayloadBytes: number; isRecent: boolean; chartTopic: string | null; chartSeries: { times: number[]; values: number[] } | null; chartFields: { path: string; label: string }[]; chartField: string; onChartFieldChange: (path: string) => void; chartSource: 'live' | 'db'; onChartSourceChange: (source: 'live' | 'db') => void; } type ViewMode = 'pretty' | 'raw' | 'tree' | 'diff'; const isJSON = (value: string) => { try { JSON.parse(value); return true; } catch { return false; } }; const isImagePayload = (payload: string) => { return payload.startsWith('data:image/') || (payload.length > 200 && /^[A-Za-z0-9+/=]+$/.test(payload)); }; const toImageDataUrl = (payload: string) => { if (payload.startsWith('data:image/')) return payload; return `data:image/jpeg;base64,${payload}`; }; const sanitizeJSON = (value: unknown): { sanitized: unknown; imageDataUrl?: string } => { if (typeof value === 'string') { if (isImagePayload(value)) { return { sanitized: '', imageDataUrl: toImageDataUrl(value) }; } return { sanitized: value }; } if (Array.isArray(value)) { let imageDataUrl: string | undefined; const sanitized = value.map((entry) => { const result = sanitizeJSON(entry); if (!imageDataUrl && result.imageDataUrl) { imageDataUrl = result.imageDataUrl; } return result.sanitized; }); return { sanitized, imageDataUrl }; } if (value && typeof value === 'object') { let imageDataUrl: string | undefined; const sanitized: Record = {}; Object.entries(value as Record).forEach(([key, entry]) => { const result = sanitizeJSON(entry); if (!imageDataUrl && result.imageDataUrl) { imageDataUrl = result.imageDataUrl; } sanitized[key] = result.sanitized; }); return { sanitized, imageDataUrl }; } return { sanitized: value }; }; const copyWithFallback = async (text: string) => { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return true; } const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', 'true'); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); const ok = document.execCommand('copy'); document.body.removeChild(textarea); return ok; }; const JsonTree: React.FC<{ value: unknown; depth?: number }> = ({ value, depth = 0 }) => { if (value === null) { return null; } if (Array.isArray(value)) { return (
{value.map((entry, idx) => (
[{idx}]
))}
); } if (typeof value === 'object') { return (
{Object.entries(value).map(([key, entry]) => (
{key}
))}
); } if (typeof value === 'number') { return {value}; } if (typeof value === 'boolean') { return {value ? 'true' : 'false'}; } return "{String(value)}"; }; export const TopicDetails: React.FC = ({ topic, lastMessage, previousMessage, maxPayloadBytes, isRecent, chartTopic, chartSeries, chartFields, chartField, onChartFieldChange, chartSource, onChartSourceChange }) => { const [viewMode, setViewMode] = useState('pretty'); const [showImage, setShowImage] = useState(false); const payloadPreview = useMemo(() => { if (!lastMessage) return ''; if (lastMessage.payload.length <= maxPayloadBytes) return lastMessage.payload; return `${lastMessage.payload.slice(0, maxPayloadBytes)}...`; }, [lastMessage, maxPayloadBytes]); if (!lastMessage) { return (
Sélectionnez un topic pour voir les détails
); } const payloadIsJSON = isJSON(lastMessage.payload); const payloadIsImage = isImagePayload(lastMessage.payload); const parsedJSON = payloadIsJSON ? JSON.parse(lastMessage.payload) : null; const parsedPreviousJSON = previousMessage && isJSON(previousMessage.payload) ? JSON.parse(previousMessage.payload) : null; const sanitized = payloadIsJSON ? sanitizeJSON(parsedJSON) : null; const imageDataUrl = sanitized?.imageDataUrl || (payloadIsImage ? toImageDataUrl(lastMessage.payload) : undefined); return (

{topic}

{new Date(lastMessage.timestamp).toLocaleTimeString()} QoS: {lastMessage.qos} {lastMessage.retained && RETAINED} Size: {lastMessage.size} B
Topic
Archivage actif (SQLite)
{imageDataUrl && (
DETECTION IMAGE BASE64
MQTT Payload setShowImage(true)} />
)} {showImage && (
setShowImage(false)} >
event.stopPropagation()} > {imageDataUrl && MQTT Payload}
)} {viewMode === 'pretty' && (
{payloadIsJSON ? (
                {JSON.stringify(sanitized?.sanitized ?? parsedJSON, null, 2)}
              
) : (
                {payloadPreview}
              
)}
)} {viewMode === 'raw' && (
              {payloadPreview}
            
)} {viewMode === 'tree' && (
{payloadIsJSON ? : 'Payload non JSON.'}
)} {viewMode === 'diff' && (
Précédent
                {previousMessage ? previousMessage.payload : 'Aucun message précédent.'}
              
Dernier
                {payloadPreview}
              
{payloadIsJSON && parsedPreviousJSON && (
Diff JSON (référence)
Avant
{JSON.stringify(parsedPreviousJSON, null, 2)}
Après
{JSON.stringify(sanitized?.sanitized ?? parsedJSON, null, 2)}
)}
)}
); };