TTMT.ManageWebGUI/src/routes/_auth/commands/index.tsx

407 lines
13 KiB
TypeScript
Raw Normal View History

2025-09-24 16:13:57 +07:00
import { createFileRoute } from "@tanstack/react-router";
2025-12-11 14:29:06 +07:00
import { useState } from "react";
import { CommandSubmitTemplate } from "@/template/command-submit-template";
import { CommandRegistryForm, type CommandRegistryFormData } from "@/components/forms/command-registry-form";
2025-12-22 14:53:19 +07:00
import {
useGetCommandList,
useGetRoomList,
useAddCommand,
useUpdateCommand,
useDeleteCommand,
useSendCommand,
} from "@/hooks/queries";
2025-09-24 16:13:57 +07:00
import { toast } from "sonner";
2025-12-11 14:29:06 +07:00
import { Check, X, Edit2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
2026-01-18 22:52:19 +07:00
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
2025-12-22 14:53:19 +07:00
import type { ColumnDef } from "@tanstack/react-table";
2025-12-11 14:29:06 +07:00
import type { ShellCommandData } from "@/components/forms/command-form";
2026-01-18 22:52:19 +07:00
import type { CommandRegistry } from "@/types/command-registry";
2025-09-24 16:13:57 +07:00
2026-03-04 14:41:34 +07:00
export const Route = createFileRoute("/_auth/commands/")({
2025-12-11 14:29:06 +07:00
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
2025-09-24 16:13:57 +07:00
component: CommandPage,
2026-03-04 14:41:34 +07:00
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Quản lý lệnh", path: "/_auth/commands/" },
];
},
2025-09-24 16:13:57 +07:00
});
function CommandPage() {
2025-12-11 14:29:06 +07:00
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedCommand, setSelectedCommand] = useState<CommandRegistry | null>(null);
2026-01-18 22:52:19 +07:00
const [detailPanelCommand, setDetailPanelCommand] = useState<CommandRegistry | null>(null);
2025-12-11 14:29:06 +07:00
const [table, setTable] = useState<any>();
// Fetch commands
2025-12-22 14:53:19 +07:00
const { data: commands = [], isLoading } = useGetCommandList();
2025-12-11 14:29:06 +07:00
// Fetch rooms
2025-12-22 14:53:19 +07:00
const { data: roomData = [] } = useGetRoomList();
2025-09-26 17:56:55 +07:00
2025-12-11 14:29:06 +07:00
const commandList: CommandRegistry[] = Array.isArray(commands)
? commands.map((cmd: any) => ({
...cmd,
qoS: cmd.qoS ?? 0,
isRetained: cmd.isRetained ?? false,
}))
2025-12-22 14:53:19 +07:00
: [];
2025-12-11 14:29:06 +07:00
2025-12-22 14:53:19 +07:00
// Mutations
const addCommandMutation = useAddCommand();
const updateCommandMutation = useUpdateCommand();
const deleteCommandMutation = useDeleteCommand();
const sendCommandMutation = useSendCommand();
2025-12-11 14:29:06 +07:00
// Columns for command table
const columns: ColumnDef<CommandRegistry>[] = [
{
accessorKey: "commandName",
header: "Tên lệnh",
2026-01-18 22:52:19 +07:00
size: 100,
2025-12-11 14:29:06 +07:00
cell: ({ getValue }) => (
2026-01-18 22:52:19 +07:00
<div className="max-w-[100px]">
<span className="font-semibold truncate block">{getValue() as string}</span>
</div>
2025-12-11 14:29:06 +07:00
),
},
2026-01-18 22:52:19 +07:00
{
accessorKey: "commandType",
header: "Loại lệnh",
cell: ({ getValue }) => {
const type = getValue() as number;
const typeMap: Record<number, string> = {
1: "RESTART",
2: "SHUTDOWN",
3: "TASKKILL",
4: "BLOCK",
};
return <span>{typeMap[type] || "UNKNOWN"}</span>;
},
},
2025-12-11 14:29:06 +07:00
{
accessorKey: "description",
header: "Mô tả",
2026-01-18 22:52:19 +07:00
size: 120,
2025-12-11 14:29:06 +07:00
cell: ({ getValue }) => (
2026-01-18 22:52:19 +07:00
<div className="max-w-[120px]">
<span className="text-sm text-muted-foreground truncate block">
{(getValue() as string) || "-"}
</span>
</div>
2025-12-11 14:29:06 +07:00
),
},
{
accessorKey: "commandContent",
header: "Nội dung lệnh",
2026-01-18 22:52:19 +07:00
size: 130,
2025-12-11 14:29:06 +07:00
cell: ({ getValue }) => (
2026-01-18 22:52:19 +07:00
<div className="max-w-[130px]">
<code className="text-xs bg-muted/50 px-1.5 py-0.5 rounded truncate block">
{(getValue() as string).substring(0, 40)}...
</code>
</div>
2025-12-11 14:29:06 +07:00
),
},
{
accessorKey: "qoS",
header: "QoS",
cell: ({ getValue }) => {
const qos = getValue() as number | undefined;
const qosValue = qos !== undefined ? qos : 0;
const colors = {
0: "text-blue-600",
1: "text-amber-600",
2: "text-red-600",
};
return (
<span className={colors[qosValue as 0 | 1 | 2]}>{qosValue}</span>
);
},
},
{
accessorKey: "isRetained",
header: "Lưu trữ",
cell: ({ getValue }) => {
const retained = getValue() as boolean;
return retained ? (
<div className="flex items-center gap-1">
<Check className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600"></span>
</div>
) : (
<div className="flex items-center gap-1">
<X className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-400">Không</span>
</div>
);
},
},
{
id: "select",
header: () => <div className="text-center text-xs">Chọn đ thực thi</div>,
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected?.() ?? false}
onChange={row.getToggleSelectedHandler?.()}
/>
),
enableSorting: false,
enableHiding: false,
},
{
id: "actions",
header: () => <div className="text-center text-xs">Hành đng</div>,
cell: ({ row }) => (
<div className="flex gap-2 justify-center">
<Button
size="sm"
variant="ghost"
2026-01-18 22:52:19 +07:00
onClick={(e) => {
e.stopPropagation();
2025-12-11 14:29:06 +07:00
setSelectedCommand(row.original);
setIsDialogOpen(true);
}}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
2026-01-18 22:52:19 +07:00
onClick={(e) => {
e.stopPropagation();
handleDeleteCommand(row.original.id);
}}
2025-12-11 14:29:06 +07:00
disabled={deleteCommandMutation.isPending}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
),
enableSorting: false,
enableHiding: false,
},
];
// Handle form submit
const handleFormSubmit = async (data: CommandRegistryFormData) => {
2025-12-22 14:53:19 +07:00
try {
if (selectedCommand) {
// Update
await updateCommandMutation.mutateAsync({
commandId: selectedCommand.id,
data,
});
} else {
// Add
await addCommandMutation.mutateAsync(data);
}
setIsDialogOpen(false);
setSelectedCommand(null);
toast.success(selectedCommand ? "Cập nhật lệnh thành công!" : "Thêm lệnh thành công!");
} catch (error) {
console.error("Form submission error:", error);
toast.error(selectedCommand ? "Cập nhật lệnh thất bại!" : "Thêm lệnh thất bại!");
2025-12-11 14:29:06 +07:00
}
};
// Handle delete
const handleDeleteCommand = async (commandId: number) => {
if (!confirm("Bạn có chắc muốn xóa lệnh này?")) return;
try {
2025-12-22 14:53:19 +07:00
await deleteCommandMutation.mutateAsync(commandId);
toast.success("Xóa lệnh thành công!");
2025-12-11 14:29:06 +07:00
} catch (error) {
console.error("Delete error:", error);
2025-12-22 14:53:19 +07:00
toast.error("Xóa lệnh thất bại!");
2025-12-11 14:29:06 +07:00
}
};
// Handle execute commands from list
const handleExecuteSelected = async (targets: string[]) => {
if (!table) {
toast.error("Không thể lấy thông tin bảng!");
return;
}
const selectedRows = table.getSelectedRowModel().rows;
if (selectedRows.length === 0) {
toast.error("Vui lòng chọn ít nhất một lệnh để thực thi!");
return;
}
try {
for (const target of targets) {
for (const row of selectedRows) {
// API expects PascalCase directly
const apiData = {
Command: row.original.commandContent,
QoS: row.original.qoS,
IsRetained: row.original.isRetained,
};
2025-12-22 14:53:19 +07:00
await sendCommandMutation.mutateAsync({
roomName: target,
2025-12-11 14:29:06 +07:00
data: apiData as any,
});
}
}
toast.success("Đã gửi yêu cầu thực thi lệnh cho các mục đã chọn!");
if (table) {
table.setRowSelection({});
}
} catch (error) {
toast.error("Có lỗi xảy ra khi thực thi!");
}
};
// Handle execute custom command
const handleExecuteCustom = async (targets: string[], commandData: ShellCommandData) => {
try {
for (const target of targets) {
// API expects PascalCase directly
const apiData = {
Command: commandData.command,
QoS: commandData.qos,
IsRetained: commandData.isRetained,
};
2025-12-22 14:53:19 +07:00
await sendCommandMutation.mutateAsync({
roomName: target,
2025-12-11 14:29:06 +07:00
data: apiData as any,
2025-09-26 17:56:55 +07:00
});
2025-12-11 14:29:06 +07:00
}
toast.success("Đã gửi lệnh tùy chỉnh cho các mục đã chọn!");
} catch (error) {
toast.error("Gửi lệnh tùy chỉnh thất bại!");
}
};
return (
2026-01-18 22:52:19 +07:00
<>
<CommandSubmitTemplate
title="Gửi lệnh từ xa"
description="Quản lý và thực thi các lệnh trên thiết bị"
data={commandList}
isLoading={isLoading}
columns={columns}
dialogOpen={isDialogOpen}
onDialogOpen={setIsDialogOpen}
dialogTitle={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"}
onAddNew={() => {
setSelectedCommand(null);
setIsDialogOpen(true);
}}
onTableInit={setTable}
formContent={
<CommandRegistryForm
onSubmit={handleFormSubmit}
closeDialog={() => setIsDialogOpen(false)}
initialData={selectedCommand || undefined}
title={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"}
/>
}
onExecuteSelected={handleExecuteSelected}
onExecuteCustom={handleExecuteCustom}
isExecuting={sendCommandMutation.isPending}
rooms={roomData}
onRowClick={(row) => setDetailPanelCommand(row)}
scrollable={true}
maxHeight="500px"
2026-03-04 16:43:45 +07:00
enablePagination
defaultPageSize={10}
2026-01-18 22:52:19 +07:00
/>
{/* Detail Dialog Popup */}
<Dialog open={!!detailPanelCommand} onOpenChange={(open) => !open && setDetailPanelCommand(null)}>
<DialogContent className="max-w-2xl max-h-[85vh]">
<DialogHeader>
<DialogTitle>Chi tiết lệnh</DialogTitle>
</DialogHeader>
{detailPanelCommand && (
<div className="space-y-6 max-h-[calc(85vh-120px)] overflow-y-auto pr-2">
{/* Command Name */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Tên lệnh</h3>
<p className="text-base font-medium break-words">{detailPanelCommand.commandName}</p>
</div>
{/* Command Type */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Loại lệnh</h3>
<p className="text-base">
{
{
1: "RESTART",
2: "SHUTDOWN",
3: "TASKKILL",
4: "BLOCK",
}[detailPanelCommand.commandType] || "UNKNOWN"
}
</p>
</div>
{/* Description */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2"> tả</h3>
<p className="text-sm text-foreground whitespace-pre-wrap break-words">
{detailPanelCommand.description || "-"}
</p>
</div>
{/* Command Content */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Nội dung lệnh</h3>
<div className="bg-muted/50 p-4 rounded-md border">
<code className="text-sm whitespace-pre-wrap break-all block font-mono">
{detailPanelCommand.commandContent}
</code>
</div>
</div>
{/* QoS */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">QoS</h3>
<p className="text-base">
<span
className={
{
0: "text-blue-600",
1: "text-amber-600",
2: "text-red-600",
}[(detailPanelCommand.qoS ?? 0) as 0 | 1 | 2]
}
>
{detailPanelCommand.qoS ?? 0}
</span>
</p>
</div>
{/* Retention */}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Lưu trữ</h3>
<div className="flex items-center gap-2">
{detailPanelCommand.isRetained ? (
<>
<Check className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600"></span>
</>
) : (
<>
<X className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-400">Không</span>
</>
)}
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
2025-09-24 16:13:57 +07:00
);
}