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

322 lines
9.8 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";
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 }) => {
// Read active tab from URL search params (client-side) to reflect breadcrumb
let activeTab = "list";
try {
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
activeTab = params.get("tab") || "list";
}
} catch (e) {
activeTab = "list";
}
2026-03-04 14:41:34 +07:00
context.breadcrumbs = [
{ title: "Quản lý lệnh", path: "/_auth/commands/" },
{
title: activeTab === "execute" ? "Lệnh thủ công" : "Danh sách",
path: `/ _auth/commands/?tab=${activeTab}`,
},
2026-03-04 14:41:34 +07:00
];
},
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);
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: () => <div className="min-w-[220px] whitespace-normal">Tên lệnh</div>,
2026-01-18 22:52:19 +07:00
size: 100,
cell: ({ getValue, row }) => {
const full = (getValue() as string) || row.original.commandName || "";
return (
<div className="min-w-[220px] whitespace-normal break-words">
<span className="font-semibold block leading-tight">{full}</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: "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">Thực thi</div>,
2025-12-11 14:29:06 +07:00
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à gửi yêu cầu thực thi các lệnh trên thiết bị"
2026-01-18 22:52:19 +07:00
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}
scrollable={true}
maxHeight="500px"
enablePagination={false}
2026-03-04 16:43:45 +07:00
defaultPageSize={10}
2026-01-18 22:52:19 +07:00
/>
</>
2025-09-24 16:13:57 +07:00
);
}