Path: blob/master/gate-controller/components/ConfiguredRelayCard.tsx
1072 views
"use client";12import { invoke } from "@tauri-apps/api/core";3import { Button } from "@/components/ui/button";4import { Card, CardContent, CardHeader } from "@/components/ui/card";5import {6AlertDialog,7AlertDialogAction,8AlertDialogCancel,9AlertDialogContent,10AlertDialogDescription,11AlertDialogFooter,12AlertDialogHeader,13AlertDialogTitle,14AlertDialogTrigger,15} from "@/components/ui/alert-dialog";16import { useState } from "react";17import { toast } from "sonner";18import {19HardDrive,20Usb,21Trash2,22ClipboardCopy,23ChevronDown,24ChevronUp,25} from "lucide-react";26import { ApiInstructionsModal } from "@/components/ApiInstructionsModal";2728interface ConfiguredRelay {29id: string;30type: "ch340" | "hw348" | "cp210x";31channels?: number;32}3334interface ConfiguredRelayCardProps {35relay: ConfiguredRelay;36onRelayRemoved: () => void;37}3839export function ConfiguredRelayCard({40relay,41onRelayRemoved,42}: ConfiguredRelayCardProps) {43const [showApiInstructions, setShowApiInstructions] = useState(false);44const [isExpanded, setIsExpanded] = useState(false);4546const handleAction = async (47action: "on" | "off",48channel: number | null = null,49) => {50try {51await invoke("trigger_relay_action", {52payload: { id: relay.id, action, channel },53});54toast.success(55`Action '${action}' sent to ${relay.id}${56channel ? ` on channel ${channel}` : ""57}`,58);59} catch (error) {60toast.error(`Failed to trigger ${relay.id}`, {61description: String(error),62});63}64};6566const handleRemove = async () => {67try {68await invoke("remove_relay", { id: relay.id });69toast.success("Relay Removed", {70description: `${relay.id} has been removed from configuration.`,71});72onRelayRemoved();73} catch (error) {74toast.error("Failed to remove relay", { description: String(error) });75}76};7778const isSerial = relay.type === "ch340" || relay.type === "cp210x";7980return (81<Card className="hover:shadow-md transition-shadow">82<CardHeader className="p-3">83<div className="flex items-center justify-between">84<div className="flex items-center gap-2 flex-1 min-w-0">85<div className="flex-shrink-0">86{isSerial ? (87<div className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">88<HardDrive className="w-4 h-4 text-primary" />89</div>90) : (91<div className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">92<Usb className="w-4 h-4 text-primary" />93</div>94)}95</div>96<div className="flex-1 min-w-0">97<h3 className="text-sm font-semibold truncate">{relay.id}</h3>98<p className="text-xs text-muted-foreground">99{relay.type === "ch340" && `CH340`}100{relay.type === "hw348" && `HW-348`}101{relay.type === "cp210x" && `CP210x`}102{relay.channels && ` • ${relay.channels}ch`}103</p>104</div>105</div>106<div className="flex items-center gap-1 flex-shrink-0">107{relay.channels && relay.channels > 0 && (108<Button109variant="ghost"110size="sm"111className="h-8 w-8 p-0"112onClick={() => setIsExpanded(!isExpanded)}113>114{isExpanded ? (115<ChevronUp className="w-4 h-4" />116) : (117<ChevronDown className="w-4 h-4" />118)}119</Button>120)}121<Button122variant="ghost"123size="sm"124className="h-8 w-8 p-0"125onClick={() => setShowApiInstructions(true)}126>127<ClipboardCopy className="w-4 h-4" />128</Button>129<AlertDialog>130<AlertDialogTrigger asChild>131<Button132variant="ghost"133size="sm"134className="h-8 w-8 p-0 text-destructive hover:text-destructive"135>136<Trash2 className="w-4 h-4" />137</Button>138</AlertDialogTrigger>139<AlertDialogContent>140<AlertDialogHeader>141<AlertDialogTitle>Are you sure?</AlertDialogTitle>142<AlertDialogDescription>143This will permanently remove the relay{" "}144<strong>{relay.id}</strong> from your configuration.145</AlertDialogDescription>146</AlertDialogHeader>147<AlertDialogFooter>148<AlertDialogCancel>Cancel</AlertDialogCancel>149<AlertDialogAction onClick={handleRemove}>150Continue151</AlertDialogAction>152</AlertDialogFooter>153</AlertDialogContent>154</AlertDialog>155</div>156</div>157</CardHeader>158159{isExpanded &&160relay.channels &&161relay.channels > 0 && (162<CardContent className="p-3 pt-0">163<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">164{[...Array(relay.channels)].map((_, i) => {165const channel = i + 1;166return (167<div key={channel} className="flex flex-col gap-1">168<span className="text-xs font-mono text-center text-muted-foreground">169Ch {channel}170</span>171<Button172size="sm"173className="bg-green-600 hover:bg-green-700 text-xs h-7 px-2"174onClick={() => handleAction("on", channel)}175>176ON177</Button>178<Button179size="sm"180className="bg-red-600 hover:bg-red-700 text-xs h-7 px-2"181onClick={() => handleAction("off", channel)}182>183OFF184</Button>185</div>186);187})}188</div>189</CardContent>190)}191192<ApiInstructionsModal193relay={relay}194isOpen={showApiInstructions}195onClose={() => setShowApiInstructions(false)}196/>197</Card>198);199}200201202