Initial commit: Refactor AgvAutoRunControls
This commit is contained in:
134
components/SerialConsole.tsx
Normal file
134
components/SerialConsole.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import { LogEntry } from '../types';
|
||||
import { Terminal, Plug, Unplug, Settings2, Trash2, Usb } from 'lucide-react';
|
||||
|
||||
interface SerialConsoleProps {
|
||||
title: string;
|
||||
logs: LogEntry[];
|
||||
isConnected: boolean;
|
||||
onConnect: () => void;
|
||||
onDisconnect: () => void;
|
||||
onClear: () => void;
|
||||
baudRate: number;
|
||||
setBaudRate: (rate: number) => void;
|
||||
portInfo?: string | null;
|
||||
colorClass?: string; // Text color for the source label
|
||||
}
|
||||
|
||||
const BAUD_RATES = [9600, 19200, 38400, 57600, 115200];
|
||||
|
||||
const SerialConsole: React.FC<SerialConsoleProps> = ({
|
||||
title,
|
||||
logs,
|
||||
isConnected,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onClear,
|
||||
baudRate,
|
||||
setBaudRate,
|
||||
portInfo,
|
||||
colorClass = 'text-gray-400'
|
||||
}) => {
|
||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Memoize reversed logs to prevent unnecessary array operations on every render
|
||||
const reversedLogs = useMemo(() => [...logs].reverse(), [logs]);
|
||||
|
||||
// Force scroll to top whenever logs update to ensure the newest (top) is visible
|
||||
useEffect(() => {
|
||||
if (logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [reversedLogs]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-gray-950 border-r border-gray-800 last:border-r-0 overflow-hidden relative group">
|
||||
{/* Header with Connection Controls */}
|
||||
<div className="flex items-center justify-between px-2 py-1.5 bg-gray-900 border-b border-gray-800 shrink-0">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<div className={`font-bold text-xs ${colorClass} flex items-center gap-1.5 whitespace-nowrap`}>
|
||||
<Terminal size={12} /> {title}
|
||||
</div>
|
||||
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${isConnected ? 'bg-green-500 shadow-[0_0_5px_rgba(34,197,94,0.6)]' : 'bg-red-500/50'}`} />
|
||||
|
||||
{isConnected && portInfo && (
|
||||
<div className="hidden sm:flex items-center gap-1 text-[9px] text-gray-500 bg-black/20 px-1.5 py-0.5 rounded border border-gray-800/50 truncate max-w-[100px]">
|
||||
<Usb size={9} />
|
||||
{portInfo.replace('ID:', '')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<select
|
||||
value={baudRate}
|
||||
onChange={(e) => setBaudRate(Number(e.target.value))}
|
||||
disabled={isConnected}
|
||||
className="bg-gray-800 text-[9px] text-gray-300 border border-gray-700 rounded px-1 py-0.5 outline-none focus:border-blue-500 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{BAUD_RATES.map(rate => <option key={rate} value={rate}>{rate}</option>)}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={isConnected ? onDisconnect : onConnect}
|
||||
title={isConnected ? "Disconnect" : "Connect"}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
isConnected
|
||||
? 'text-red-400 hover:bg-red-900/30'
|
||||
: 'text-blue-400 hover:bg-blue-900/30'
|
||||
}`}
|
||||
>
|
||||
{isConnected ? <Unplug size={14} /> : <Plug size={14} />}
|
||||
</button>
|
||||
|
||||
<div className="w-px h-3 bg-gray-800 mx-0.5"></div>
|
||||
|
||||
<button
|
||||
onClick={onClear}
|
||||
title="Clear Logs"
|
||||
className="p-1 text-gray-500 hover:text-gray-200 hover:bg-gray-800 rounded transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Body */}
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
className="flex-1 overflow-y-auto p-2 font-mono text-[10px] space-y-0.5 leading-tight bg-black"
|
||||
>
|
||||
{reversedLogs.length === 0 && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-gray-800 space-y-1">
|
||||
<Settings2 size={20} className="opacity-20" />
|
||||
<span className="text-[9px]">No Data</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reversedLogs.map((log, index) => (
|
||||
<div key={log.id} className="flex gap-2 break-all opacity-90 hover:opacity-100 hover:bg-gray-900/30 py-[1px] border-b border-gray-900/50 last:border-0 items-start">
|
||||
{/* Row Number (1 = Newest) */}
|
||||
<span className="text-gray-700 select-none min-w-[20px] text-right text-[9px] pt-0.5">{index + 1}</span>
|
||||
|
||||
<span className="text-gray-600 select-none whitespace-nowrap min-w-[45px] text-[9px] pt-0.5">{log.timestamp}</span>
|
||||
<span className={`font-bold select-none whitespace-nowrap w-4 text-center text-[9px] pt-0.5 ${
|
||||
log.type === 'TX' ? 'text-green-600' : log.type === 'RX' ? 'text-purple-500' : 'text-gray-600'
|
||||
}`}>
|
||||
{log.type === 'RX' ? 'RX' : log.type === 'TX' ? 'TX' : '--'}
|
||||
</span>
|
||||
<span className={`flex-1 font-mono break-all
|
||||
${log.type === 'TX' ? 'text-green-200/90' : ''}
|
||||
${log.type === 'RX' ? 'text-purple-200/90' : ''}
|
||||
${log.type === 'ERROR' ? 'text-red-400' : ''}
|
||||
${log.type === 'INFO' ? 'text-gray-500 italic' : ''}
|
||||
`}>
|
||||
{log.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SerialConsole;
|
||||
Reference in New Issue
Block a user