261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
import { useState } from 'react';
|
|
import { Bluetooth, Globe, Wifi, Trash2, Scan } from 'lucide-react';
|
|
import WiFiScanOverlay from './WiFiScanOverlay';
|
|
import { toast } from 'sonner';
|
|
import { useWs } from '../ws';
|
|
|
|
interface NetworkPageProps {
|
|
config: any;
|
|
setConfig: (config: any) => void;
|
|
}
|
|
|
|
export default function NetworkPage({ config, setConfig }: NetworkPageProps) {
|
|
const { send: wsSend } = useWs();
|
|
const [showWiFiScan, setShowWiFiScan] = useState(false);
|
|
|
|
const updateSetting = (path: string[], value: any) => {
|
|
const newConfig = JSON.parse(JSON.stringify(config));
|
|
let current = newConfig;
|
|
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
current = current[path[i]];
|
|
}
|
|
|
|
current[path[path.length - 1]] = value;
|
|
setConfig(newConfig);
|
|
};
|
|
|
|
const network = config.network || {};
|
|
const bluetooth = config.bluetooth || {};
|
|
const wifi = config.wifi || [];
|
|
|
|
const removeWifiNetwork = (index: number) => {
|
|
const network = wifi[index];
|
|
const newWifi = wifi.filter((_: any, i: number) => i !== index);
|
|
updateSetting(['wifi'], newWifi);
|
|
toast.success(`Removed ${network.ssid}`);
|
|
};
|
|
|
|
const addWifiNetwork = (ssid: string, passphrase: string) => {
|
|
// Disconnect all existing networks
|
|
const updatedWifi = wifi.map((network: any) => ({
|
|
...network,
|
|
enabled: 0
|
|
}));
|
|
|
|
// Add new network at the top with enabled status
|
|
const newWifi = [{ ssid, passphrase, enabled: 1 }, ...updatedWifi];
|
|
updateSetting(['wifi'], newWifi);
|
|
};
|
|
|
|
return (
|
|
<div className="p-4 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-sm text-neutral-500 flex items-center gap-2"><Wifi className="w-4 h-4" /> Known WiFi Networks</h2>
|
|
<button
|
|
onClick={() => { wsSend('scan'); setShowWiFiScan(true); }}
|
|
className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg"
|
|
>
|
|
<Scan className="w-4 h-4" />
|
|
Scan
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
|
{wifi.map((network: any, index: number) => (
|
|
<div key={index} className="p-4 flex items-center justify-between">
|
|
<button
|
|
className="flex items-center gap-3 flex-1 min-w-0 text-left hover:bg-neutral-50 -m-1 p-1 rounded"
|
|
onClick={() => {
|
|
const cmd = network.passphrase
|
|
? `connect ${network.ssid} ${network.passphrase}`
|
|
: `connect ${network.ssid}`;
|
|
wsSend(cmd);
|
|
toast.info(`Connecting to ${network.ssid}…`);
|
|
const updated = wifi
|
|
.map((n: any, i: number) => ({ ...n, enabled: i === index ? 1 : 0 }))
|
|
.sort((a: any, b: any) => b.enabled - a.enabled);
|
|
updateSetting(['wifi'], updated);
|
|
}}
|
|
>
|
|
<Wifi className={`w-5 h-5 flex-shrink-0 ${network.enabled ? 'text-blue-600' : 'text-neutral-400'}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium truncate">{network.ssid}</div>
|
|
{network.enabled === 1 && (
|
|
<div className="text-xs text-green-600">Connected</div>
|
|
)}
|
|
</div>
|
|
</button>
|
|
<button
|
|
onClick={() => removeWifiNetwork(index)}
|
|
className="p-2 text-red-600 hover:bg-red-50 rounded ml-2"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
|
|
{wifi.length === 0 && (
|
|
<div className="p-8 text-center text-neutral-500 text-sm">
|
|
No WiFi networks configured
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showWiFiScan && (
|
|
<WiFiScanOverlay
|
|
onClose={() => setShowWiFiScan(false)}
|
|
onNetworkAdded={addWifiNetwork}
|
|
existingNetworks={wifi.map((n: any) => n.ssid)}
|
|
/>
|
|
)}
|
|
|
|
<h2 className="text-sm text-neutral-500 pt-4 flex items-center gap-2"><Globe className="w-4 h-4" /> Network</h2>
|
|
|
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
|
<div className="p-4">
|
|
<label className="text-sm text-neutral-500 block mb-2">Hostname</label>
|
|
<input
|
|
type="text"
|
|
value={network.hostname || ''}
|
|
onChange={(e) => updateSetting(['network', 'hostname'], e.target.value)}
|
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<div className="p-4">
|
|
<label className="text-sm text-neutral-500 block mb-2">SNTP Server</label>
|
|
<input
|
|
type="text"
|
|
value={network.sntpserver || ''}
|
|
onChange={(e) => updateSetting(['network', 'sntpserver'], e.target.value)}
|
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<div className="p-4 flex items-center justify-between">
|
|
<label className="text-sm text-neutral-500">HTTPD</label>
|
|
<button
|
|
onClick={() => updateSetting(['network', 'services', 'httpd', 'enabled'], network.services?.httpd?.enabled ? 0 : 1)}
|
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
|
network.services?.httpd?.enabled ? 'bg-blue-600' : 'bg-neutral-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
|
network.services?.httpd?.enabled ? 'translate-x-6' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-4 flex items-center justify-between">
|
|
<label className="text-sm text-neutral-500">WebSockets</label>
|
|
<button
|
|
onClick={() => updateSetting(['network', 'services', 'httpd', 'websockets'], network.services?.httpd?.websockets ? 0 : 1)}
|
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
|
network.services?.httpd?.websockets ? 'bg-blue-600' : 'bg-neutral-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
|
network.services?.httpd?.websockets ? 'translate-x-6' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-4 flex items-center justify-between">
|
|
<label className="text-sm text-neutral-500">mDNS</label>
|
|
<button
|
|
onClick={() => updateSetting(['network', 'services', 'mdns'], network.services?.mdns ? 0 : 1)}
|
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
|
network.services?.mdns ? 'bg-blue-600' : 'bg-neutral-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
|
network.services?.mdns ? 'translate-x-6' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-4 flex items-center justify-between">
|
|
<label className="text-sm text-neutral-500">SSDP</label>
|
|
<button
|
|
onClick={() => updateSetting(['network', 'services', 'ssdp'], network.services?.ssdp ? 0 : 1)}
|
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
|
network.services?.ssdp ? 'bg-blue-600' : 'bg-neutral-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
|
network.services?.ssdp ? 'translate-x-6' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-4 flex items-center justify-between">
|
|
<label className="text-sm text-neutral-500">TCP Console</label>
|
|
<button
|
|
onClick={() => updateSetting(['network', 'services', 'tcp_console'], network.services?.tcp_console ? 0 : 1)}
|
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
|
network.services?.tcp_console ? 'bg-blue-600' : 'bg-neutral-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
|
network.services?.tcp_console ? 'translate-x-6' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 className="text-sm text-neutral-500 pt-4 flex items-center gap-2"><Bluetooth className="w-4 h-4" /> Bluetooth</h2>
|
|
|
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
|
|
|
<div className="p-4 flex items-center justify-between">
|
|
<label className="text-sm text-neutral-500">Enabled</label>
|
|
<button
|
|
onClick={() => updateSetting(['bluetooth', 'enabled'], bluetooth.enabled ? 0 : 1)}
|
|
className={`relative w-12 h-6 rounded-full transition-colors ${
|
|
bluetooth.enabled ? 'bg-blue-600' : 'bg-neutral-300'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
|
bluetooth.enabled ? 'translate-x-6' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-4">
|
|
<label className="text-sm text-neutral-500 block mb-2">Device Name</label>
|
|
<input
|
|
type="text"
|
|
value={bluetooth.devicename || ''}
|
|
onChange={(e) => updateSetting(['bluetooth', 'devicename'], e.target.value)}
|
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
|
|
<div className="p-4">
|
|
<label className="text-sm text-neutral-500 block mb-2">Baud Rate</label>
|
|
<input
|
|
type="number"
|
|
value={bluetooth.baud || ''}
|
|
onChange={(e) => updateSetting(['bluetooth', 'baud'], parseInt(e.target.value))}
|
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|