diff --git a/src/components/buttons/command-action-buttons.tsx b/src/components/buttons/command-action-buttons.tsx
new file mode 100644
index 0000000..c865080
--- /dev/null
+++ b/src/components/buttons/command-action-buttons.tsx
@@ -0,0 +1,275 @@
+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 { useGetCommandsByTypes } from "@/hooks/queries/useCommandQueries";
+import { useSendCommand } from "@/hooks/queries";
+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",
+ },
+};
+
+export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) {
+ const [confirmDialog, setConfirmDialog] = useState<{
+ open: boolean;
+ command: any;
+ commandType: CommandType;
+ }>({
+ open: false,
+ command: null,
+ commandType: CommandType.RESTART,
+ });
+ const [isExecuting, setIsExecuting] = useState(false);
+
+ // Query commands for each type
+ const { data: restartCommands = [] } = useGetCommandsByTypes(CommandType.RESTART.toString());
+ const { data: shutdownCommands = [] } = useGetCommandsByTypes(CommandType.SHUTDOWN.toString());
+ const { data: taskkillCommands = [] } = useGetCommandsByTypes(CommandType.TASKKILL.toString());
+ const { data: blockCommands = [] } = useGetCommandsByTypes(CommandType.BLOCK.toString());
+
+ // Send command mutation
+ const sendCommandMutation = useSendCommand();
+
+ const commandsByType = {
+ [CommandType.RESTART]: restartCommands,
+ [CommandType.SHUTDOWN]: shutdownCommands,
+ [CommandType.TASKKILL]: taskkillCommands,
+ [CommandType.BLOCK]: blockCommands,
+ };
+
+ const handleCommandClick = (command: any, commandType: CommandType) => {
+ setConfirmDialog({
+ open: true,
+ command,
+ commandType,
+ });
+ };
+
+ const handleConfirmExecute = async () => {
+ setIsExecuting(true);
+ try {
+ // Chuẩn bị data theo format API (PascalCase)
+ const apiData = {
+ Command: confirmDialog.command.commandContent,
+ QoS: confirmDialog.command.qoS,
+ IsRetained: confirmDialog.command.isRetained,
+ };
+
+ // Gửi lệnh đến phòng
+ await sendCommandMutation.mutateAsync({
+ roomName,
+ data: apiData as any,
+ });
+
+ toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`);
+ setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART });
+
+ // 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 });
+ // 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 (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {commands.map((command: any) => (
+ handleCommandClick(command, commandType)}
+ className="cursor-pointer"
+ >
+
+ {command.commandName}
+ {command.description && (
+
+ {command.description}
+
+ )}
+
+
+ ))}
+
+
+ );
+ };
+
+ return (
+ <>
+
+ {Object.values(CommandType)
+ .filter((value) => typeof value === "number")
+ .map((commandType) => renderCommandButton(commandType as CommandType))}
+
+
+ {/* Confirm Dialog */}
+
+ >
+ );
+}
diff --git a/src/components/forms/command-registry-form.tsx b/src/components/forms/command-registry-form.tsx
index 1628dc5..d615bde 100644
--- a/src/components/forms/command-registry-form.tsx
+++ b/src/components/forms/command-registry-form.tsx
@@ -16,6 +16,7 @@ import { Info } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { z } from "zod";
+import { CommandType } from "@/types/command-registry";
interface CommandRegistryFormProps {
onSubmit: (data: CommandRegistryFormData) => Promise;
@@ -26,6 +27,7 @@ interface CommandRegistryFormProps {
export interface CommandRegistryFormData {
commandName: string;
+ commandType: CommandType;
description?: string;
commandContent: string;
qos: 0 | 1 | 2;
@@ -40,6 +42,9 @@ const commandRegistrySchema = z.object({
.min(3, "Tên lệnh phải có ít nhất 3 ký tự")
.max(100, "Tên lệnh tối đa 100 ký tự")
.trim(),
+ commandType: z.nativeEnum(CommandType, {
+ errorMap: () => ({ message: "Loại lệnh không hợp lệ" }),
+ }),
description: z.string().max(500, "Mô tả tối đa 500 ký tự").optional(),
commandContent: z
.string()
@@ -88,6 +93,7 @@ export function CommandRegistryForm({
const form = useForm({
defaultValues: {
commandName: initialData?.commandName || "",
+ commandType: initialData?.commandType || CommandType.RESTART,
description: initialData?.description || "",
commandContent: initialData?.commandContent || "",
qos: (initialData?.qos || 0) as 0 | 1 | 2,
@@ -159,6 +165,37 @@ export function CommandRegistryForm({
)}
+ {/* Loại lệnh */}
+
+ {(field: any) => (
+
+
+
+ {field.state.meta.errors?.length > 0 && (
+
+ {String(field.state.meta.errors[0])}
+
+ )}
+
+ Phân loại lệnh để dễ dàng quản lý và tổ chức
+
+
+ )}
+
+
{/* Mô tả */}
{(field: any) => (
diff --git a/src/components/tables/version-table.tsx b/src/components/tables/version-table.tsx
index 261e8b0..9ddbf6c 100644
--- a/src/components/tables/version-table.tsx
+++ b/src/components/tables/version-table.tsx
@@ -12,6 +12,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { ScrollArea } from "@/components/ui/scroll-area";
import { useEffect } from "react";
interface VersionTableProps {
@@ -19,6 +20,9 @@ interface VersionTableProps {
columns: ColumnDef[];
isLoading: boolean;
onTableInit?: (table: any) => void;
+ onRowClick?: (row: TData) => void;
+ scrollable?: boolean;
+ maxHeight?: string;
}
export function VersionTable({
@@ -26,6 +30,9 @@ export function VersionTable({
columns,
isLoading,
onTableInit,
+ onRowClick,
+ scrollable = false,
+ maxHeight = "calc(100vh - 320px)",
}: VersionTableProps) {
const table = useReactTable({
data,
@@ -39,7 +46,7 @@ export function VersionTable({
onTableInit?.(table);
}, [table, onTableInit]);
- return (
+ const tableContent = (
{table.getHeaderGroups().map((headerGroup) => (
@@ -72,6 +79,8 @@ export function VersionTable({
onRowClick?.(row.original)}
+ className={onRowClick ? "cursor-pointer hover:bg-muted/50" : ""}
>
{row.getVisibleCells().map((cell) => (
@@ -84,4 +93,16 @@ export function VersionTable({
);
+
+ if (scrollable) {
+ return (
+
+
+ {tableContent}
+
+
+ );
+ }
+
+ return {tableContent}
;
}
diff --git a/src/config/api.ts b/src/config/api.ts
index e3fac17..ccf9f4c 100644
--- a/src/config/api.ts
+++ b/src/config/api.ts
@@ -51,6 +51,7 @@ export const API_ENDPOINTS = {
{
ADD_COMMAND: `${BASE_URL}/Command/add`,
GET_COMMANDS: `${BASE_URL}/Command/all`,
+ GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${commandId}`,
DELETE_COMMAND: (commandId: number) => `${BASE_URL}/Command/delete/${commandId}`,
},
diff --git a/src/hooks/queries/useCommandQueries.ts b/src/hooks/queries/useCommandQueries.ts
index 3e14346..f90e1d2 100644
--- a/src/hooks/queries/useCommandQueries.ts
+++ b/src/hooks/queries/useCommandQueries.ts
@@ -19,6 +19,16 @@ export function useGetCommandList(enabled = true) {
});
}
+//Hook để lấy lệnh theo loại
+export function useGetCommandsByTypes(types: string, enabled = true) {
+ return useQuery({
+ queryKey: [...COMMAND_QUERY_KEYS.all, "by-types", types],
+ queryFn: () => commandService.getCommandsByTypes(types),
+ enabled,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ });
+}
+
/**
* Hook để thêm lệnh mới
*/
diff --git a/src/routes/_auth.tsx b/src/routes/_auth.tsx
index 798f3cf..bbd386e 100644
--- a/src/routes/_auth.tsx
+++ b/src/routes/_auth.tsx
@@ -3,12 +3,12 @@ import AppLayout from '@/layouts/app-layout'
export const Route = createFileRoute('/_auth')({
- beforeLoad: async ({context}) => {
- const {token} = context.auth
- if (!token) {
- throw redirect({to: '/login'})
- }
- },
+ // beforeLoad: async ({context}) => {
+ // const {token} = context.auth
+ // if (!token) {
+ // throw redirect({to: '/login'})
+ // }
+ // },
component: AuthenticatedLayout,
})
diff --git a/src/routes/_auth/command/index.tsx b/src/routes/_auth/command/index.tsx
index 9e7b794..b6cad20 100644
--- a/src/routes/_auth/command/index.tsx
+++ b/src/routes/_auth/command/index.tsx
@@ -13,19 +13,12 @@ import {
import { toast } from "sonner";
import { Check, X, Edit2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { ScrollArea } from "@/components/ui/scroll-area";
import type { ColumnDef } from "@tanstack/react-table";
import type { ShellCommandData } from "@/components/forms/command-form";
-
-interface CommandRegistry {
- id: number;
- commandName: string;
- description?: string;
- commandContent: string;
- qoS: 0 | 1 | 2;
- isRetained: boolean;
- createdAt?: string;
- updatedAt?: string;
-}
+import type { CommandRegistry } from "@/types/command-registry";
export const Route = createFileRoute("/_auth/command/")({
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
@@ -35,6 +28,7 @@ export const Route = createFileRoute("/_auth/command/")({
function CommandPage() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedCommand, setSelectedCommand] = useState(null);
+ const [detailPanelCommand, setDetailPanelCommand] = useState(null);
const [table, setTable] = useState();
// Fetch commands
@@ -62,26 +56,49 @@ function CommandPage() {
{
accessorKey: "commandName",
header: "Tên lệnh",
+ size: 100,
cell: ({ getValue }) => (
- {getValue() as string}
+
+ {getValue() as string}
+
),
},
+ {
+ accessorKey: "commandType",
+ header: "Loại lệnh",
+ cell: ({ getValue }) => {
+ const type = getValue() as number;
+ const typeMap: Record = {
+ 1: "RESTART",
+ 2: "SHUTDOWN",
+ 3: "TASKKILL",
+ 4: "BLOCK",
+ };
+ return {typeMap[type] || "UNKNOWN"};
+ },
+ },
{
accessorKey: "description",
header: "Mô tả",
+ size: 120,
cell: ({ getValue }) => (
-
- {(getValue() as string) || "-"}
-
+
+
+ {(getValue() as string) || "-"}
+
+
),
},
{
accessorKey: "commandContent",
header: "Nội dung lệnh",
+ size: 130,
cell: ({ getValue }) => (
-
- {(getValue() as string).substring(0, 100)}...
-
+
+
+ {(getValue() as string).substring(0, 40)}...
+
+
),
},
{
@@ -139,7 +156,8 @@ function CommandPage() {