feat: added command sent in cluster panel
This commit is contained in:
+66
-1
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
AlertCircle, Antenna, Bell, CheckCircle2, Clock, CloudOff, Compass, Database, Ear, Eraser, Hash, Loader2, Lock,
|
||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Trash2, Unlock, X, Zap,
|
||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -912,6 +912,31 @@ export default function App() {
|
||||
const [clusterFilterSource, setClusterFilterSource] = useState<number | ''>('');
|
||||
const [clusterGroup, setClusterGroup] = useState(true);
|
||||
const [clusterCmd, setClusterCmd] = useState('');
|
||||
// Cluster console: the raw traffic. Spots are parsed out of the stream into the
|
||||
// grid; everything else (SH/DX output, WHO, MOTD, errors) used to be discarded,
|
||||
// so a command appeared to do nothing at all.
|
||||
type ClusterLine = { server_id: number; server_name: string; text: string; sent: boolean; at: string };
|
||||
const CONSOLE_CAP = 2000; // a busy cluster runs for hours — don't grow forever
|
||||
const [clusterLines, setClusterLines] = useState<ClusterLine[]>([]);
|
||||
const [clusterConsoleOpen, setClusterConsoleOpen] = useState(false);
|
||||
const clusterConsoleRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const off = EventsOn('cluster:line', (l: any) => {
|
||||
setClusterLines((prev) => {
|
||||
const next = [...prev, l as ClusterLine];
|
||||
return next.length > CONSOLE_CAP ? next.slice(next.length - CONSOLE_CAP) : next;
|
||||
});
|
||||
});
|
||||
return () => { off?.(); };
|
||||
}, []);
|
||||
// Follow the tail, but ONLY when already at the bottom — otherwise scrolling up
|
||||
// to read a SH/DX reply would yank you back down on the next spot.
|
||||
useEffect(() => {
|
||||
const el = clusterConsoleRef.current;
|
||||
if (!el) return;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
||||
if (atBottom) el.scrollTop = el.scrollHeight;
|
||||
}, [clusterLines]);
|
||||
// Multi-band filter: empty set = all bands. The user toggles chips.
|
||||
const [clusterBands, setClusterBands] = useState<Set<string>>(new Set());
|
||||
// Lock-to-entry: when on, the band filter follows the entry's current
|
||||
@@ -4336,8 +4361,48 @@ export default function App() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Console — the RAW cluster stream. Spots are parsed out of it into
|
||||
the grid above, but SH/DX, WHO, the MOTD and error replies are not
|
||||
spots: without this they were dropped and the command box looked
|
||||
inert (you typed SH/DX/100 and nothing ever happened). */}
|
||||
{clusterConsoleOpen && (
|
||||
<div className="border-t border-border/60 shrink-0 flex flex-col" style={{ height: 200 }}>
|
||||
<div className="flex items-center gap-2 px-2.5 py-1 bg-muted/30 border-b border-border/60 shrink-0">
|
||||
<Terminal className="size-3.5 text-muted-foreground" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t('cluster.console')}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">{clusterLines.length}</span>
|
||||
<div className="flex-1" />
|
||||
<button className="text-[11px] text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setClusterLines([])}>{t('cluster.clear')}</button>
|
||||
<button className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setClusterConsoleOpen(false)} title={t('cluster.hideConsole')}>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div ref={clusterConsoleRef} className="flex-1 min-h-0 overflow-auto bg-background/40 px-2.5 py-1.5 font-mono text-[11px] leading-[1.45]">
|
||||
{clusterLines.length === 0 ? (
|
||||
<p className="text-muted-foreground italic">{t('cluster.consoleEmpty')}</p>
|
||||
) : clusterLines.map((l, i) => (
|
||||
<div key={i} className={cn('whitespace-pre-wrap break-all', l.sent ? 'text-primary font-semibold' : 'text-foreground/85')}>
|
||||
<span className="text-muted-foreground/60 mr-1.5 select-none">{l.at}</span>
|
||||
{l.sent && <span className="text-muted-foreground/60 mr-1 select-none">»</span>}
|
||||
{l.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Command input — sends to the master server. */}
|
||||
<div className="flex items-center gap-2 p-2.5 border-t border-border/60 shrink-0">
|
||||
{!clusterConsoleOpen && (
|
||||
<button onClick={() => setClusterConsoleOpen(true)} title={t('cluster.showConsole')}
|
||||
className="inline-flex items-center justify-center size-8 rounded-md border border-border hover:bg-muted shrink-0">
|
||||
<Terminal className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground font-mono whitespace-nowrap">→ master</span>
|
||||
<Input
|
||||
className="font-mono text-xs h-8"
|
||||
|
||||
Reference in New Issue
Block a user