291 lines
9.3 KiB
TypeScript
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 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>
|
|
)}
|
|
{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 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 || (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>
|
|
</>
|
|
);
|
|
}
|