TTMT.ManageWebGUI/src/components/buttons/command-action-buttons.tsx

291 lines
9.3 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 { useGetSensitiveCommands, useExecuteSensitiveCommand } from "@/hooks/queries/useCommandQueries";
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",
},
[CommandType.RESET]: {
label : "Reset",
icon: Loader2,
color: "text-green-600",
bgColor: "bg-green-50 hover:bg-green-100",
}
};
export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) {
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean;
command: any;
commandType: CommandType;
isSensitive?: boolean;
}>({
open: false,
command: null,
commandType: CommandType.RESTART,
});
const [isExecuting, setIsExecuting] = useState(false);
const [sensitivePassword, setSensitivePassword] = useState("");
// Query commands for each type
const { data: sensitiveCommands = [] } = useGetSensitiveCommands();
// Send command mutation (sensitive)
const executeSensitiveMutation = useExecuteSensitiveCommand();
// Build commands mapped by CommandType using the `command` field from sensitive data
const commandsByType: Record<number, any[]> = (Object.values(CommandType) as Array<number | string>)
.filter((v) => typeof v === "number")
.reduce((acc: Record<number, any[]>, type) => {
acc[type as number] = (sensitiveCommands || []).filter((c: any) => Number(c.command) === Number(type));
return acc;
}, {} as Record<number, any[]>);
const handleCommandClick = (command: any, commandType: CommandType) => {
// When building from sensitiveCommands, all items here are sensitive
setConfirmDialog({
open: true,
command,
commandType,
isSensitive: true,
});
};
const handleConfirmExecute = async () => {
setIsExecuting(true);
try {
// All rendered commands are sourced from sensitiveCommands — send via sensitive mutation
await executeSensitiveMutation.mutateAsync({
roomName,
command: confirmDialog.command.commandContent,
password: sensitivePassword,
});
toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`);
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART, isSensitive: false });
setSensitivePassword("");
// 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, isSensitive: false });
setSensitivePassword("");
// 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 flex-shrink-0"
>
<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 flex-shrink-0`}
>
<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 gap-2 flex-nowrap overflow-x-auto items-center whitespace-nowrap">
{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 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>
)}
{confirmDialog.isSensitive && (
<div className="mt-2">
<label className="block text-sm font-medium mb-1">Mật khẩu</label>
<input
type="password"
value={sensitivePassword}
onChange={(e) => setSensitivePassword(e.target.value)}
className="w-full px-2 py-1 rounded border"
placeholder="Nhập mật khẩu để xác nhận"
/>
</div>
)}
<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 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 || (confirmDialog.isSensitive && !sensitivePassword)}
className="gap-2"
>
{isExecuting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Đang thực thi...
</>
) : (
"Xác nhận"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}