TTMT.ManageWebGUI/src/components/forms/command-registry-form.tsx

417 lines
16 KiB
TypeScript

import { useForm } from "@tanstack/react-form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
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<void>;
closeDialog?: () => void;
initialData?: Partial<CommandRegistryFormData>;
title?: string;
}
export interface CommandRegistryFormData {
commandName: string;
commandType: CommandType;
description?: string;
commandContent: string;
qos: 0 | 1 | 2;
isRetained: boolean;
}
// Zod validation schema
const commandRegistrySchema = z.object({
commandName: z
.string()
.min(1, "Tên lệnh không được để trống")
.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()
.min(1, "Nội dung lệnh không được để trống")
.min(5, "Nội dung lệnh phải có ít nhất 5 ký tự")
.trim(),
qos: z.union([z.literal(0), z.literal(1), z.literal(2)]),
isRetained: z.boolean(),
});
const QoSLevels = [
{
level: 0,
name: "At Most Once (Fire and Forget)",
description:
"Gửi lệnh một lần mà không đảm bảo. Nếu broker hoặc client bị ngắt kết nối, lệnh có thể bị mất. Tốc độ nhanh nhất, tiêu tốn ít tài nguyên.",
useCase: "Các lệnh không quan trọng, có thể mất mà không ảnh hưởng",
},
{
level: 1,
name: "At Least Once",
description:
"Đảm bảo lệnh sẽ được nhận ít nhất một lần. Có thể gửi lại nếu chưa nhận được ACK. Lệnh có thể được nhận nhiều lần.",
useCase: "Hầu hết các lệnh bình thường cần đảm bảo gửi thành công",
},
{
level: 2,
name: "Exactly Once",
description:
"Đảm bảo lệnh được nhận chính xác một lần. Sử dụng bắt tay 4 chiều để đảm bảo độ tin cậy cao nhất. Tốc độ chậm hơn, tiêu tốn nhiều tài nguyên.",
useCase: "Các lệnh quan trọng như xóa dữ liệu, thay đổi cấu hình",
},
];
export function CommandRegistryForm({
onSubmit,
closeDialog,
initialData,
title = "Đăng ký Lệnh Mới",
}: CommandRegistryFormProps) {
const [selectedQoS, setSelectedQoS] = useState<number>(
initialData?.qos ?? 0
);
const [isSubmitting, setIsSubmitting] = useState(false);
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,
isRetained: initialData?.isRetained || false,
},
onSubmit: async ({ value }) => {
try {
// Validate using Zod
const validatedData = commandRegistrySchema.parse(value);
setIsSubmitting(true);
await onSubmit(validatedData as CommandRegistryFormData);
toast.success("Lưu lệnh thành công!");
if (closeDialog) {
closeDialog();
}
} catch (error: any) {
if (error.errors?.length > 0) {
toast.error(error.errors[0].message);
} else {
console.error("Submit error:", error);
toast.error("Có lỗi xảy ra khi lưu lệnh!");
}
} finally {
setIsSubmitting(false);
}
},
});
return (
<div className="w-full space-y-6">
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>
Tạo cấu hình lệnh MQTT mới đ điều khiển thiết bị
</CardDescription>
</CardHeader>
<CardContent>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
{/* Tên lệnh */}
<form.Field name="commandName">
{(field: any) => (
<div className="space-y-2">
<Label>
Tên Lệnh <span className="text-red-500">*</span>
</Label>
<Input
placeholder="VD: RestartDevice, ShutdownPC, UpdateSoftware..."
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={isSubmitting}
/>
{field.state.meta.errors?.length > 0 && (
<p className="text-sm text-red-500">
{String(field.state.meta.errors[0])}
</p>
)}
<p className="text-sm text-muted-foreground">
Tên đnh danh duy nhất cho lệnh này
</p>
</div>
)}
</form.Field>
{/* Loại lệnh */}
<form.Field name="commandType">
{(field: any) => (
<div className="space-y-2">
<Label>
Loại Lệnh <span className="text-red-500">*</span>
</Label>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2"
value={field.state.value}
onChange={(e) => field.handleChange(Number(e.target.value))}
onBlur={field.handleBlur}
disabled={isSubmitting}
>
<option value={CommandType.RESTART}>RESTART - Khởi đng lại</option>
<option value={CommandType.SHUTDOWN}>SHUTDOWN - Tắt máy</option>
<option value={CommandType.TASKKILL}>TASKKILL - Kết thúc tác vụ</option>
<option value={CommandType.BLOCK}>BLOCK - Chặn</option>
</select>
{field.state.meta.errors?.length > 0 && (
<p className="text-sm text-red-500">
{String(field.state.meta.errors[0])}
</p>
)}
<p className="text-sm text-muted-foreground">
Phân loại lệnh đ dễ dàng quản tổ chức
</p>
</div>
)}
</form.Field>
{/* Mô tả */}
<form.Field name="description">
{(field: any) => (
<div className="space-y-2">
<Label> Tả (Tùy chọn)</Label>
<Textarea
placeholder="Nhập mô tả chi tiết về lệnh này..."
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={isSubmitting}
rows={3}
/>
{field.state.meta.errors?.length > 0 && (
<p className="text-sm text-red-500">
{String(field.state.meta.errors[0])}
</p>
)}
<p className="text-sm text-muted-foreground">
tả chi tiết về chức năng cách sử dụng lệnh
</p>
</div>
)}
</form.Field>
{/* Nội dung lệnh */}
<form.Field name="commandContent">
{(field: any) => (
<div className="space-y-2">
<Label>
Nội Dung Lệnh <span className="text-red-500">*</span>
</Label>
<Textarea
placeholder="VD: shutdown /s /t 30 /c 'Máy sẽ tắt trong 30 giây'"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={isSubmitting}
rows={5}
className="font-mono text-sm"
/>
{field.state.meta.errors?.length > 0 && (
<p className="text-sm text-red-500">
{String(field.state.meta.errors[0])}
</p>
)}
<p className="text-sm text-muted-foreground">
Nội dung lệnh sẽ đưc gửi tới thiết bị (PowerShell, CMD, bash...)
</p>
</div>
)}
</form.Field>
{/* QoS Level */}
<form.Field name="qos">
{(field: any) => (
<div className="space-y-2">
<Label>
QoS (Quality of Service) <span className="text-red-500">*</span>
</Label>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2"
value={field.state.value}
onChange={(e) => {
const value = Number(e.target.value) as 0 | 1 | 2;
field.handleChange(value);
setSelectedQoS(value);
}}
onBlur={field.handleBlur}
disabled={isSubmitting}
>
<option value="0">QoS 0 - At Most Once (Tốc đ cao)</option>
<option value="1">QoS 1 - At Least Once (Cân bằng)</option>
<option value="2">QoS 2 - Exactly Once (Đ tin cậy cao)</option>
</select>
{field.state.meta.errors?.length > 0 && (
<p className="text-sm text-red-500">
{String(field.state.meta.errors[0])}
</p>
)}
</div>
)}
</form.Field>
{/* Chú thích QoS */}
{selectedQoS !== null && (
<Alert className="border-l-4 border-l-blue-500 bg-blue-50">
<Info className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-sm space-y-3 mt-2">
<div>
<div className="font-semibold text-blue-900">
{QoSLevels[selectedQoS].name}
</div>
<div className="text-blue-800 mt-1">
{QoSLevels[selectedQoS].description}
</div>
<div className="text-blue-700 mt-2">
<span className="font-medium">Trường hợp sử dụng:</span>{" "}
{QoSLevels[selectedQoS].useCase}
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* Bảng so sánh QoS */}
<Card className="bg-muted/50">
<CardHeader className="pb-3">
<CardTitle className="text-base">
Bảng So Sánh Các Mức QoS
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b">
<th className="text-left py-2 px-2 font-semibold">
Tiêu Chí
</th>
<th className="text-center py-2 px-2 font-semibold">
QoS 0
</th>
<th className="text-center py-2 px-2 font-semibold">
QoS 1
</th>
<th className="text-center py-2 px-2 font-semibold">
QoS 2
</th>
</tr>
</thead>
<tbody>
<tr className="border-b bg-white">
<td className="py-2 px-2">Đm bảo gửi</td>
<td className="text-center">Không</td>
<td className="text-center"></td>
<td className="text-center">Chính xác</td>
</tr>
<tr className="border-b bg-white">
<td className="py-2 px-2">Tốc đ</td>
<td className="text-center">Nhanh nhất</td>
<td className="text-center">Trung bình</td>
<td className="text-center">Chậm nhất</td>
</tr>
<tr className="border-b bg-white">
<td className="py-2 px-2">Tài nguyên</td>
<td className="text-center">Ít nhất</td>
<td className="text-center">Trung bình</td>
<td className="text-center">Nhiều nhất</td>
</tr>
<tr className="border-b bg-white">
<td className="py-2 px-2">Đ tin cậy</td>
<td className="text-center">Thấp</td>
<td className="text-center">Cao</td>
<td className="text-center">Cao nhất</td>
</tr>
<tr className="bg-white">
<td className="py-2 px-2">Số lần nhận tối đa</td>
<td className="text-center">1 (hoặc 0)</td>
<td className="text-center"> 1</td>
<td className="text-center">1</td>
</tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* IsRetained Checkbox */}
<form.Field name="isRetained">
{(field: any) => (
<div className="flex items-center gap-3 rounded-lg border p-4">
<Checkbox
checked={field.state.value}
onCheckedChange={field.handleChange}
disabled={isSubmitting}
/>
<div className="flex-1">
<Label className="text-base cursor-pointer">
Lưu giữ lệnh (Retained)
</Label>
<p className="text-sm text-muted-foreground mt-1">
Broker MQTT sẽ lưu lệnh này gửi cho client mới khi
kết nối. Hữu ích cho các lệnh cấu hình cần duy trì trạng
thái.
</p>
</div>
</div>
)}
</form.Field>
{/* Submit Button */}
<div className="flex gap-3 pt-4">
<Button
type="submit"
disabled={isSubmitting}
className="flex-1"
>
{isSubmitting ? "Đang lưu..." : "Lưu Lệnh"}
</Button>
{closeDialog && (
<Button
type="button"
variant="outline"
disabled={isSubmitting}
className="flex-1"
onClick={closeDialog}
>
Hủy
</Button>
)}
</div>
</form>
</CardContent>
</Card>
</div>
);
}