322 lines
9.8 KiB
TypeScript
322 lines
9.8 KiB
TypeScript
import { createFileRoute } from "@tanstack/react-router";
|
|
import { useState } from "react";
|
|
import { CommandSubmitTemplate } from "@/template/command-submit-template";
|
|
import { CommandRegistryForm, type CommandRegistryFormData } from "@/components/forms/command-registry-form";
|
|
import {
|
|
useGetCommandList,
|
|
useGetRoomList,
|
|
useAddCommand,
|
|
useUpdateCommand,
|
|
useDeleteCommand,
|
|
useSendCommand,
|
|
} from "@/hooks/queries";
|
|
import { toast } from "sonner";
|
|
import { Check, X, Edit2, Trash2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import type { ColumnDef } from "@tanstack/react-table";
|
|
import type { ShellCommandData } from "@/components/forms/command-form";
|
|
import type { CommandRegistry } from "@/types/command-registry";
|
|
|
|
export const Route = createFileRoute("/_auth/commands/")({
|
|
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
|
|
component: CommandPage,
|
|
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";
|
|
}
|
|
|
|
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}`,
|
|
},
|
|
];
|
|
},
|
|
});
|
|
|
|
function CommandPage() {
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [selectedCommand, setSelectedCommand] = useState<CommandRegistry | null>(null);
|
|
const [table, setTable] = useState<any>();
|
|
|
|
// Fetch commands
|
|
const { data: commands = [], isLoading } = useGetCommandList();
|
|
|
|
// Fetch rooms
|
|
const { data: roomData = [] } = useGetRoomList();
|
|
|
|
const commandList: CommandRegistry[] = Array.isArray(commands)
|
|
? commands.map((cmd: any) => ({
|
|
...cmd,
|
|
qoS: cmd.qoS ?? 0,
|
|
isRetained: cmd.isRetained ?? false,
|
|
}))
|
|
: [];
|
|
|
|
// Mutations
|
|
const addCommandMutation = useAddCommand();
|
|
const updateCommandMutation = useUpdateCommand();
|
|
const deleteCommandMutation = useDeleteCommand();
|
|
const sendCommandMutation = useSendCommand();
|
|
|
|
// Columns for command table
|
|
const columns: ColumnDef<CommandRegistry>[] = [
|
|
{
|
|
accessorKey: "commandName",
|
|
header: () => <div className="min-w-[220px] whitespace-normal">Tên lệnh</div>,
|
|
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>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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>;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "commandContent",
|
|
header: "Nội dung lệnh",
|
|
size: 130,
|
|
cell: ({ getValue }) => (
|
|
<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>
|
|
),
|
|
},
|
|
{
|
|
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">Có</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>,
|
|
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"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedCommand(row.original);
|
|
setIsDialogOpen(true);
|
|
}}
|
|
>
|
|
<Edit2 className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDeleteCommand(row.original.id);
|
|
}}
|
|
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) => {
|
|
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!");
|
|
}
|
|
};
|
|
|
|
// Handle delete
|
|
const handleDeleteCommand = async (commandId: number) => {
|
|
if (!confirm("Bạn có chắc muốn xóa lệnh này?")) return;
|
|
|
|
try {
|
|
await deleteCommandMutation.mutateAsync(commandId);
|
|
toast.success("Xóa lệnh thành công!");
|
|
} catch (error) {
|
|
console.error("Delete error:", error);
|
|
toast.error("Xóa lệnh thất bại!");
|
|
}
|
|
};
|
|
|
|
// 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,
|
|
};
|
|
|
|
await sendCommandMutation.mutateAsync({
|
|
roomName: target,
|
|
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,
|
|
};
|
|
await sendCommandMutation.mutateAsync({
|
|
roomName: target,
|
|
data: apiData as any,
|
|
});
|
|
}
|
|
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 (
|
|
<>
|
|
<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ị"
|
|
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}
|
|
defaultPageSize={10}
|
|
/>
|
|
</>
|
|
);
|
|
}
|