This repository has been archived on 2026-08-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
droidfleet-visor/src/components/AdbConsole.tsx

225 lines
8.4 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { AndroidVM } from '../types';
import {
Terminal,
Send,
Copy,
Check,
Trash2,
Play,
ShieldAlert,
Cpu,
Layers,
Search,
} from 'lucide-react';
interface AdbConsoleProps {
vm: AndroidVM;
vms: AndroidVM[];
onSelectVm: (vmId: string) => void;
onExecuteAdb: (command: string) => Promise<string>;
}
export const AdbConsole: React.FC<AdbConsoleProps> = ({
vm,
vms,
onSelectVm,
onExecuteAdb,
}) => {
const [commandInput, setCommandInput] = useState('');
const [terminalHistory, setTerminalHistory] = useState<
{ command: string; output: string; timestamp: string }[]
>([
{
command: 'adb connect 192.168.122.105:5555',
output: `* daemon not running; starting now at tcp:5037\n* daemon started successfully\nconnected to ${vm.ipAddress}:${vm.adbPort}`,
timestamp: new Date().toLocaleTimeString(),
},
]);
const [isExecuting, setIsExecuting] = useState(false);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const terminalEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
terminalEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [terminalHistory]);
const handleRunCommand = async (cmdToRun?: string) => {
const cmd = (cmdToRun || commandInput).trim();
if (!cmd) return;
setIsExecuting(true);
const result = await onExecuteAdb(cmd);
setTerminalHistory((prev) => [
...prev,
{
command: cmd,
output: result,
timestamp: new Date().toLocaleTimeString(),
},
]);
setCommandInput('');
setIsExecuting(false);
};
const presetCommands = [
{ label: 'System Build Props', cmd: 'getprop' },
{ label: 'List Packages', cmd: 'pm list packages' },
{ label: 'Check Root Privilege', cmd: 'su' },
{ label: 'CPU Info', cmd: 'cat /proc/cpuinfo' },
{ label: 'Battery Dumpsys', cmd: 'dumpsys battery' },
{ label: 'Live Logcat', cmd: 'logcat -d' },
];
const handleCopy = (text: string, index: number) => {
navigator.clipboard.writeText(text);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
return (
<div className="space-y-6">
{/* Target VM Selector & Network Status Bar */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-slate-900/80 p-4 rounded-2xl border border-slate-800">
<div className="flex items-center space-x-3 w-full sm:w-auto">
<Terminal className="w-5 h-5 text-emerald-400" />
<div className="flex items-center space-x-2">
<span className="text-xs font-bold text-slate-300">Target Instance:</span>
<select
value={vm.id}
onChange={(e) => onSelectVm(e.target.value)}
className="bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-emerald-400 font-bold focus:outline-none focus:border-emerald-500"
>
{vms.map((v) => (
<option key={v.id} value={v.id}>
{v.name} ({v.ipAddress}:{v.adbPort})
</option>
))}
</select>
</div>
</div>
<div className="flex items-center space-x-3 text-xs font-mono">
<span className="px-2.5 py-1 bg-emerald-950/60 border border-emerald-800 text-emerald-300 rounded-lg">
ADB TCP: {vm.ipAddress}:{vm.adbPort}
</span>
<span className="px-2.5 py-1 bg-slate-950 border border-slate-800 text-slate-400 rounded-lg uppercase">
SELinux: {vm.bootloader.selinuxMode}
</span>
</div>
</div>
{/* Preset Quick Commands */}
<div className="space-y-2">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">
Quick ADB Shell Presets:
</span>
<div className="flex items-center space-x-2 overflow-x-auto pb-1">
{presetCommands.map((preset) => (
<button
key={preset.cmd}
onClick={() => handleRunCommand(preset.cmd)}
disabled={isExecuting}
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 hover:border-emerald-500/50 rounded-xl text-xs font-mono whitespace-nowrap transition-all flex items-center space-x-1.5 shrink-0"
>
<Play className="w-3 h-3 text-emerald-400" />
<span>{preset.label}</span>
</button>
))}
</div>
</div>
{/* Interactive ADB Shell Terminal View */}
<div className="bg-slate-950 border border-slate-800 rounded-2xl overflow-hidden flex flex-col h-[520px] shadow-2xl">
{/* Terminal Header */}
<div className="bg-slate-900 px-4 py-3 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-rose-500/80" />
<div className="w-3 h-3 rounded-full bg-amber-500/80" />
<div className="w-3 h-3 rounded-full bg-emerald-500/80" />
<span className="text-xs font-mono text-slate-400 ml-2">
adb shell shell@{vm.profile.id}:/$
</span>
</div>
<button
onClick={() => setTerminalHistory([])}
className="p-1.5 text-slate-400 hover:text-rose-400 hover:bg-slate-800 rounded-lg text-xs flex items-center space-x-1 transition-colors"
title="Clear Terminal Output"
>
<Trash2 className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Clear</span>
</button>
</div>
{/* Console Log Area */}
<div className="flex-1 p-4 font-mono text-xs overflow-y-auto space-y-4">
{terminalHistory.map((item, idx) => (
<div key={idx} className="space-y-1.5 border-b border-slate-900/80 pb-3">
<div className="flex items-center justify-between text-slate-400">
<div className="flex items-center space-x-2">
<span className="text-emerald-400">$</span>
<span className="font-bold text-slate-200">{item.command}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-[10px] text-slate-600">{item.timestamp}</span>
<button
onClick={() => handleCopy(item.output, idx)}
className="p-1 text-slate-500 hover:text-slate-200"
>
{copiedIndex === idx ? (
<Check className="w-3 h-3 text-emerald-400" />
) : (
<Copy className="w-3 h-3" />
)}
</button>
</div>
</div>
<pre className="text-slate-300 bg-slate-900/60 p-3 rounded-xl border border-slate-800/80 whitespace-pre-wrap font-mono leading-relaxed overflow-x-auto text-[11px]">
{item.output}
</pre>
</div>
))}
<div ref={terminalEndRef} />
</div>
{/* Terminal Command Input */}
<div className="p-3 bg-slate-900 border-t border-slate-800">
<form
onSubmit={(e) => {
e.preventDefault();
handleRunCommand();
}}
className="flex items-center space-x-2"
>
<span className="text-emerald-400 font-mono font-bold text-xs">$</span>
<input
type="text"
value={commandInput}
onChange={(e) => setCommandInput(e.target.value)}
placeholder="Type adb shell command (e.g. getprop, logcat, su)..."
disabled={isExecuting}
className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-100 placeholder-slate-600 focus:outline-none focus:border-emerald-500"
/>
<button
type="submit"
disabled={isExecuting || !commandInput.trim()}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 text-white font-bold rounded-xl text-xs flex items-center space-x-1.5 transition-all"
>
<Send className="w-3.5 h-3.5" />
<span>Run</span>
</button>
</form>
</div>
</div>
</div>
);
};