193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import { useEffect, useMemo, useRef } from "react";
|
|
import { useWAClient } from "./context";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { createDisclosure } from "@/lib/utils";
|
|
import { FileTerminalIcon } from "lucide-react";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
type Props = {
|
|
curDeviceId: string;
|
|
setDeviceId: (id: string) => void;
|
|
};
|
|
|
|
const commandDlg = createDisclosure<{ deviceId: string }>();
|
|
|
|
export default function DevicesSection({ curDeviceId, setDeviceId }: Props) {
|
|
const { client, isReady } = useWAClient();
|
|
const { data: devices } = useQuery({
|
|
queryKey: ["devices"],
|
|
queryFn: () => client.listDevices(),
|
|
enabled: isReady,
|
|
refetchInterval: 10_000,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!curDeviceId && devices?.length && devices[0].id) {
|
|
setDeviceId(devices[0].id);
|
|
}
|
|
}, [curDeviceId, devices]);
|
|
|
|
const deviceList = useMemo(() => {
|
|
return devices?.map((i, idx) => {
|
|
const lastAlive = i.lastAlive || 0;
|
|
const isOnline = Date.now() - lastAlive <= 60_000;
|
|
|
|
return {
|
|
label: i.name || i.id || "Unknown",
|
|
value: i.id || String(idx),
|
|
lastAlive,
|
|
isOnline,
|
|
};
|
|
});
|
|
}, [devices]);
|
|
|
|
const curDevice = useMemo(() => {
|
|
return deviceList?.find((i) => i.value === curDeviceId);
|
|
}, [deviceList, curDeviceId]);
|
|
|
|
return (
|
|
<div className="border-t p-4 flex items-center gap-2">
|
|
<Select
|
|
items={deviceList}
|
|
value={curDeviceId}
|
|
onValueChange={(i) => setDeviceId(i || "")}
|
|
>
|
|
<SelectTrigger className="w-full justify-start">
|
|
<SelectValue placeholder="Select Device.." className="flex-none" />
|
|
{curDevice ? (
|
|
<span
|
|
className={
|
|
"inline-block size-3 rounded-full " +
|
|
(curDevice.isOnline ? "bg-green-500" : "bg-red-500")
|
|
}
|
|
/>
|
|
) : null}
|
|
<div className="flex-1" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{deviceList?.map((i) => (
|
|
<SelectItem
|
|
key={i.value}
|
|
value={i.value}
|
|
className="cursor-pointer"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<p>{i.label}</p>
|
|
<span
|
|
className={
|
|
"inline-block size-3 rounded-full " +
|
|
(i.isOnline ? "bg-green-500" : "bg-red-500")
|
|
}
|
|
/>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button
|
|
onClick={() => commandDlg.onOpen({ deviceId: curDeviceId })}
|
|
variant="outline"
|
|
>
|
|
<FileTerminalIcon />
|
|
</Button>
|
|
|
|
<CommandModal />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const cmdPreset = [
|
|
{ title: "Status", command: "status" },
|
|
{ title: "Has Focus", command: "return document.hasFocus();" },
|
|
{ title: "Reload", command: "window.location.reload(); return true;" },
|
|
];
|
|
|
|
function CommandModal() {
|
|
const { data, isOpen } = commandDlg.useStore();
|
|
const { client } = useWAClient();
|
|
const cmdTextRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
const cmd = useMutation({
|
|
async mutationFn(cmd: string) {
|
|
if (cmd === "status") {
|
|
return client.status();
|
|
}
|
|
return client.exec(cmd, data?.deviceId);
|
|
},
|
|
});
|
|
|
|
const onSubmit = () => {
|
|
const command = cmdTextRef.current?.value || "";
|
|
cmd.mutate(command);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={commandDlg.onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Execute Command</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div>
|
|
<Textarea ref={cmdTextRef} placeholder="cmd..." />
|
|
<div className="flex items-center gap-2 overflow-x-auto mt-2">
|
|
{cmdPreset.map((i) => (
|
|
<button
|
|
key={i.command}
|
|
onClick={() => (cmdTextRef.current!.value = i.command)}
|
|
>
|
|
<Badge className="cursor-pointer">{i.title}</Badge>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{cmd.error ? (
|
|
<div className="mt-3">
|
|
<p className="text-sm font-medium leading-tight">Error:</p>
|
|
<pre className="text-sm text-destructive mt-1">
|
|
{JSON.stringify(cmd.error, null, 2)}
|
|
</pre>
|
|
</div>
|
|
) : null}
|
|
|
|
{cmd.data !== undefined ? (
|
|
<div className="mt-3">
|
|
<p className="text-sm font-medium leading-tight">Result:</p>
|
|
<pre className="text-sm mt-1">
|
|
{JSON.stringify((cmd as any).data, null, 2)}
|
|
</pre>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<DialogClose>
|
|
<Button variant="ghost">Cancel</Button>
|
|
</DialogClose>
|
|
<Button onClick={onSubmit} disabled={cmd.isPending}>
|
|
Execute
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|