276 lines
8.4 KiB
TypeScript
276 lines
8.4 KiB
TypeScript
|
|
import { useState } from "react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from "@/components/ui/dropdown-menu";
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogDescription,
|
||
|
|
DialogFooter,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from "@/components/ui/dialog";
|
||
|
|
import { useGetCommandsByTypes } from "@/hooks/queries/useCommandQueries";
|
||
|
|
import { useSendCommand } from "@/hooks/queries";
|
||
|
|
import { CommandType } from "@/types/command-registry";
|
||
|
|
import {
|
||
|
|
Power,
|
||
|
|
PowerOff,
|
||
|
|
XCircle,
|
||
|
|
ShieldBan,
|
||
|
|
ChevronDown,
|
||
|
|
Loader2,
|
||
|
|
AlertTriangle
|
||
|
|
} from "lucide-react";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
interface CommandActionButtonsProps {
|
||
|
|
roomName: string;
|
||
|
|
selectedDevices?: string[]; // Các thiết bị đã chọn
|
||
|
|
}
|
||
|
|
|
||
|
|
const COMMAND_TYPE_CONFIG = {
|
||
|
|
[CommandType.RESTART]: {
|
||
|
|
label: "Khởi động lại",
|
||
|
|
icon: Power,
|
||
|
|
color: "text-blue-600",
|
||
|
|
bgColor: "bg-blue-50 hover:bg-blue-100",
|
||
|
|
},
|
||
|
|
[CommandType.SHUTDOWN]: {
|
||
|
|
label: "Tắt máy",
|
||
|
|
icon: PowerOff,
|
||
|
|
color: "text-red-600",
|
||
|
|
bgColor: "bg-red-50 hover:bg-red-100",
|
||
|
|
},
|
||
|
|
[CommandType.TASKKILL]: {
|
||
|
|
label: "Kết thúc tác vụ",
|
||
|
|
icon: XCircle,
|
||
|
|
color: "text-orange-600",
|
||
|
|
bgColor: "bg-orange-50 hover:bg-orange-100",
|
||
|
|
},
|
||
|
|
[CommandType.BLOCK]: {
|
||
|
|
label: "Chặn",
|
||
|
|
icon: ShieldBan,
|
||
|
|
color: "text-purple-600",
|
||
|
|
bgColor: "bg-purple-50 hover:bg-purple-100",
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) {
|
||
|
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||
|
|
open: boolean;
|
||
|
|
command: any;
|
||
|
|
commandType: CommandType;
|
||
|
|
}>({
|
||
|
|
open: false,
|
||
|
|
command: null,
|
||
|
|
commandType: CommandType.RESTART,
|
||
|
|
});
|
||
|
|
const [isExecuting, setIsExecuting] = useState(false);
|
||
|
|
|
||
|
|
// Query commands for each type
|
||
|
|
const { data: restartCommands = [] } = useGetCommandsByTypes(CommandType.RESTART.toString());
|
||
|
|
const { data: shutdownCommands = [] } = useGetCommandsByTypes(CommandType.SHUTDOWN.toString());
|
||
|
|
const { data: taskkillCommands = [] } = useGetCommandsByTypes(CommandType.TASKKILL.toString());
|
||
|
|
const { data: blockCommands = [] } = useGetCommandsByTypes(CommandType.BLOCK.toString());
|
||
|
|
|
||
|
|
// Send command mutation
|
||
|
|
const sendCommandMutation = useSendCommand();
|
||
|
|
|
||
|
|
const commandsByType = {
|
||
|
|
[CommandType.RESTART]: restartCommands,
|
||
|
|
[CommandType.SHUTDOWN]: shutdownCommands,
|
||
|
|
[CommandType.TASKKILL]: taskkillCommands,
|
||
|
|
[CommandType.BLOCK]: blockCommands,
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCommandClick = (command: any, commandType: CommandType) => {
|
||
|
|
setConfirmDialog({
|
||
|
|
open: true,
|
||
|
|
command,
|
||
|
|
commandType,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirmExecute = async () => {
|
||
|
|
setIsExecuting(true);
|
||
|
|
try {
|
||
|
|
// Chuẩn bị data theo format API (PascalCase)
|
||
|
|
const apiData = {
|
||
|
|
Command: confirmDialog.command.commandContent,
|
||
|
|
QoS: confirmDialog.command.qoS,
|
||
|
|
IsRetained: confirmDialog.command.isRetained,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Gửi lệnh đến phòng
|
||
|
|
await sendCommandMutation.mutateAsync({
|
||
|
|
roomName,
|
||
|
|
data: apiData as any,
|
||
|
|
});
|
||
|
|
|
||
|
|
toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`);
|
||
|
|
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART });
|
||
|
|
|
||
|
|
// Reload page để tránh freeze
|
||
|
|
setTimeout(() => {
|
||
|
|
window.location.reload();
|
||
|
|
}, 500);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Execute command error:", error);
|
||
|
|
toast.error("Lỗi khi gửi lệnh!");
|
||
|
|
setIsExecuting(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCloseDialog = () => {
|
||
|
|
if (!isExecuting) {
|
||
|
|
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART });
|
||
|
|
// Reload để tránh freeze
|
||
|
|
setTimeout(() => {
|
||
|
|
window.location.reload();
|
||
|
|
}, 300);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const renderCommandButton = (commandType: CommandType) => {
|
||
|
|
const config = COMMAND_TYPE_CONFIG[commandType];
|
||
|
|
const commands = commandsByType[commandType];
|
||
|
|
const Icon = config.icon;
|
||
|
|
|
||
|
|
if (!commands || commands.length === 0) {
|
||
|
|
return (
|
||
|
|
<Button
|
||
|
|
key={commandType}
|
||
|
|
variant="outline"
|
||
|
|
disabled
|
||
|
|
size="sm"
|
||
|
|
className="gap-2"
|
||
|
|
>
|
||
|
|
<Icon className={`h-4 w-4 ${config.color}`} />
|
||
|
|
{config.label}
|
||
|
|
<span className="text-xs text-muted-foreground ml-1">(0)</span>
|
||
|
|
</Button>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<DropdownMenu key={commandType}>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
className={`gap-2 ${config.bgColor} border-${config.color.split('-')[1]}-200`}
|
||
|
|
>
|
||
|
|
<Icon className={`h-4 w-4 ${config.color}`} />
|
||
|
|
{config.label}
|
||
|
|
<span className="text-xs text-muted-foreground ml-1">({commands.length})</span>
|
||
|
|
<ChevronDown className="h-3 w-3 ml-1" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent
|
||
|
|
align="start"
|
||
|
|
side="bottom"
|
||
|
|
sideOffset={4}
|
||
|
|
alignOffset={0}
|
||
|
|
className="w-64"
|
||
|
|
avoidCollisions={true}
|
||
|
|
>
|
||
|
|
{commands.map((command: any) => (
|
||
|
|
<DropdownMenuItem
|
||
|
|
key={command.id}
|
||
|
|
onClick={() => handleCommandClick(command, commandType)}
|
||
|
|
className="cursor-pointer"
|
||
|
|
>
|
||
|
|
<div className="flex flex-col gap-1">
|
||
|
|
<span className="font-medium">{command.commandName}</span>
|
||
|
|
{command.description && (
|
||
|
|
<span className="text-xs text-muted-foreground">
|
||
|
|
{command.description}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</DropdownMenuItem>
|
||
|
|
))}
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{Object.values(CommandType)
|
||
|
|
.filter((value) => typeof value === "number")
|
||
|
|
.map((commandType) => renderCommandButton(commandType as CommandType))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Confirm Dialog */}
|
||
|
|
<Dialog open={confirmDialog.open} onOpenChange={handleCloseDialog}>
|
||
|
|
<DialogContent>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<AlertTriangle className="h-5 w-5 text-orange-600" />
|
||
|
|
Xác nhận thực thi lệnh
|
||
|
|
</DialogTitle>
|
||
|
|
<DialogDescription className="text-left space-y-3">
|
||
|
|
<p>
|
||
|
|
Bạn có chắc chắn muốn thực thi lệnh <strong>{confirmDialog.command?.commandName}</strong>?
|
||
|
|
</p>
|
||
|
|
{confirmDialog.command?.description && (
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{confirmDialog.command.description}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
<div className="bg-muted p-3 rounded-md space-y-1">
|
||
|
|
<p className="text-sm">
|
||
|
|
<span className="font-medium">Phòng:</span> {roomName}
|
||
|
|
</p>
|
||
|
|
<p className="text-sm">
|
||
|
|
<span className="font-medium">Loại lệnh:</span>{" "}
|
||
|
|
{COMMAND_TYPE_CONFIG[confirmDialog.commandType]?.label}
|
||
|
|
</p>
|
||
|
|
{selectedDevices.length > 0 && (
|
||
|
|
<p className="text-sm">
|
||
|
|
<span className="font-medium">Thiết bị đã chọn:</span>{" "}
|
||
|
|
{selectedDevices.length} thiết bị
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<p className="text-sm text-orange-600 font-medium">
|
||
|
|
Lệnh sẽ được thực thi ngay lập tức và không thể hoàn tác.
|
||
|
|
</p>
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
<DialogFooter>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onClick={handleCloseDialog}
|
||
|
|
disabled={isExecuting}
|
||
|
|
>
|
||
|
|
Hủy
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
onClick={handleConfirmExecute}
|
||
|
|
disabled={isExecuting}
|
||
|
|
className="gap-2"
|
||
|
|
>
|
||
|
|
{isExecuting ? (
|
||
|
|
<>
|
||
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||
|
|
Đang thực thi...
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
"Xác nhận"
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|