change quick command button in room detail page

This commit is contained in:
Do Manh Phuong 2026-03-19 16:35:43 +07:00
parent 67f5dbbb08
commit dc7ed4c71a
17 changed files with 234 additions and 157 deletions

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" filepath="$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/src/hooks/useAuth.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/hooks/useAuth.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/routeTree.gen.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/routeTree.gen.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/blacklist/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/command/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/$roomName/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/types/app-sidebar.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/app-sidebar.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/types/permission.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/permission.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 1
}]]></component>
<component name="ProjectId" id="3AQVfIkiaizPRlnpMICDG3COfJV" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"dart.analysis.tool.window.visible": "false",
"git-widget-placeholder": "main",
"ignore.virus.scanning.warn.message": "true",
"javascript.preferred.runtime.type.id": "node",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"ts.external.directory.path": "D:\\MyProject\\NAVISProject\\TTMT.ManageWebGUI\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-WS-253.31033.133" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="" />
<created>1772524885874</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1772524885874</updated>
<workItem from="1772524887267" duration="1839000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

View File

@ -14,8 +14,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useGetCommandsByTypes } from "@/hooks/queries/useCommandQueries"; import { useGetSensitiveCommands, useExecuteSensitiveCommand } from "@/hooks/queries/useCommandQueries";
import { useSendCommand } from "@/hooks/queries";
import { CommandType } from "@/types/command-registry"; import { CommandType } from "@/types/command-registry";
import { import {
Power, Power,
@ -58,6 +57,12 @@ const COMMAND_TYPE_CONFIG = {
color: "text-purple-600", color: "text-purple-600",
bgColor: "bg-purple-50 hover:bg-purple-100", bgColor: "bg-purple-50 hover:bg-purple-100",
}, },
[CommandType.RESET]: {
label : "Reset",
icon: Loader2,
color: "text-green-600",
bgColor: "bg-green-50 hover:bg-green-100",
}
}; };
export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) { export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) {
@ -65,55 +70,52 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
open: boolean; open: boolean;
command: any; command: any;
commandType: CommandType; commandType: CommandType;
isSensitive?: boolean;
}>({ }>({
open: false, open: false,
command: null, command: null,
commandType: CommandType.RESTART, commandType: CommandType.RESTART,
}); });
const [isExecuting, setIsExecuting] = useState(false); const [isExecuting, setIsExecuting] = useState(false);
const [sensitivePassword, setSensitivePassword] = useState("");
// Query commands for each type // Query commands for each type
const { data: restartCommands = [] } = useGetCommandsByTypes(CommandType.RESTART.toString()); const { data: sensitiveCommands = [] } = useGetSensitiveCommands();
const { data: shutdownCommands = [] } = useGetCommandsByTypes(CommandType.SHUTDOWN.toString());
const { data: taskkillCommands = [] } = useGetCommandsByTypes(CommandType.TASKKILL.toString());
const { data: blockCommands = [] } = useGetCommandsByTypes(CommandType.BLOCK.toString());
// Send command mutation // Send command mutation (sensitive)
const sendCommandMutation = useSendCommand(); const executeSensitiveMutation = useExecuteSensitiveCommand();
const commandsByType = { // Build commands mapped by CommandType using the `command` field from sensitive data
[CommandType.RESTART]: restartCommands, const commandsByType: Record<number, any[]> = (Object.values(CommandType) as Array<number | string>)
[CommandType.SHUTDOWN]: shutdownCommands, .filter((v) => typeof v === "number")
[CommandType.TASKKILL]: taskkillCommands, .reduce((acc: Record<number, any[]>, type) => {
[CommandType.BLOCK]: blockCommands, acc[type as number] = (sensitiveCommands || []).filter((c: any) => Number(c.command) === Number(type));
}; return acc;
}, {} as Record<number, any[]>);
const handleCommandClick = (command: any, commandType: CommandType) => { const handleCommandClick = (command: any, commandType: CommandType) => {
// When building from sensitiveCommands, all items here are sensitive
setConfirmDialog({ setConfirmDialog({
open: true, open: true,
command, command,
commandType, commandType,
isSensitive: true,
}); });
}; };
const handleConfirmExecute = async () => { const handleConfirmExecute = async () => {
setIsExecuting(true); setIsExecuting(true);
try { try {
// Chuẩn bị data theo format API (PascalCase) // All rendered commands are sourced from sensitiveCommands — send via sensitive mutation
const apiData = { await executeSensitiveMutation.mutateAsync({
Command: confirmDialog.command.commandContent,
QoS: confirmDialog.command.qoS,
IsRetained: confirmDialog.command.isRetained,
};
// Gửi lệnh đến phòng
await sendCommandMutation.mutateAsync({
roomName, roomName,
data: apiData as any, command: confirmDialog.command.commandContent,
password: sensitivePassword,
}); });
toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`); toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`);
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART }); setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART, isSensitive: false });
setSensitivePassword("");
// Reload page để tránh freeze // Reload page để tránh freeze
setTimeout(() => { setTimeout(() => {
@ -128,7 +130,8 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
const handleCloseDialog = () => { const handleCloseDialog = () => {
if (!isExecuting) { if (!isExecuting) {
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART }); setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART, isSensitive: false });
setSensitivePassword("");
// Reload để tránh freeze // Reload để tránh freeze
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@ -148,7 +151,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
variant="outline" variant="outline"
disabled disabled
size="sm" size="sm"
className="gap-2" className="gap-2 flex-shrink-0"
> >
<Icon className={`h-4 w-4 ${config.color}`} /> <Icon className={`h-4 w-4 ${config.color}`} />
{config.label} {config.label}
@ -163,7 +166,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={`gap-2 ${config.bgColor} border-${config.color.split('-')[1]}-200`} className={`gap-2 ${config.bgColor} border-${config.color.split('-')[1]}-200 flex-shrink-0`}
> >
<Icon className={`h-4 w-4 ${config.color}`} /> <Icon className={`h-4 w-4 ${config.color}`} />
{config.label} {config.label}
@ -202,7 +205,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
return ( return (
<> <>
<div className="flex flex-wrap gap-2"> <div className="flex gap-2 flex-nowrap overflow-x-auto items-center whitespace-nowrap">
{Object.values(CommandType) {Object.values(CommandType)
.filter((value) => typeof value === "number") .filter((value) => typeof value === "number")
.map((commandType) => renderCommandButton(commandType as CommandType))} .map((commandType) => renderCommandButton(commandType as CommandType))}
@ -225,6 +228,18 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
{confirmDialog.command.description} {confirmDialog.command.description}
</p> </p>
)} )}
{confirmDialog.isSensitive && (
<div className="mt-2">
<label className="block text-sm font-medium mb-1">Mật khẩu</label>
<input
type="password"
value={sensitivePassword}
onChange={(e) => setSensitivePassword(e.target.value)}
className="w-full px-2 py-1 rounded border"
placeholder="Nhập mật khẩu để xác nhận"
/>
</div>
)}
<div className="bg-muted p-3 rounded-md space-y-1"> <div className="bg-muted p-3 rounded-md space-y-1">
<p className="text-sm"> <p className="text-sm">
<span className="font-medium">Phòng:</span> {roomName} <span className="font-medium">Phòng:</span> {roomName}
@ -255,7 +270,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
</Button> </Button>
<Button <Button
onClick={handleConfirmExecute} onClick={handleConfirmExecute}
disabled={isExecuting} disabled={isExecuting || (confirmDialog.isSensitive && !sensitivePassword)}
className="gap-2" className="gap-2"
> >
{isExecuting ? ( {isExecuting ? (

View File

@ -1,8 +1,10 @@
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Monitor, Wifi, WifiOff } from "lucide-react"; import { Monitor, Wifi, WifiOff, Loader2 } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FolderStatusPopover } from "../folder-status-popover"; import { FolderStatusPopover } from "../folder-status-popover";
import { useGetClientFolderStatusForDevice } from "@/hooks/queries";
import type { ClientFolderStatus } from "@/types/folder"; import type { ClientFolderStatus } from "@/types/folder";
export function ComputerCard({ export function ComputerCard({
device, device,
@ -31,6 +33,62 @@ export function ComputerCard({
const firstNetworkInfo = device.networkInfos?.[0]; const firstNetworkInfo = device.networkInfos?.[0];
const agentVersion = device.version; const agentVersion = device.version;
function DeviceFolderCheck() {
const deviceId = device.id;
const room = device.room;
const [checking, setChecking] = useState(false);
const { data: status, isLoading } = useGetClientFolderStatusForDevice(
deviceId,
room,
checking
);
const handleCheck = () => setChecking((s) => !s);
return (
<div>
<button
onClick={handleCheck}
className="inline-flex items-center gap-2 px-3 py-1 rounded border bg-background text-sm"
>
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
Kiểm tra thư mục Setup
</button>
{checking && isLoading && (
<div className="text-xs text-muted-foreground mt-2">Đang kiểm tra...</div>
)}
{checking && !isLoading && status && (
<div className="text-xs mt-2">
<div className="font-medium">Các file trong thư mục Setup({status.currentFiles?.length ?? 0})</div>
<div className="mt-1 max-h-36 overflow-auto space-y-1">
{(status.currentFiles ?? []).length === 0 ? (
<div className="text-muted-foreground">Không file hiện tại</div>
) : (
(status.currentFiles ?? []).map((f: any) => (
<div key={f.fileName} className="font-mono text-xs">
<div className="truncate">{f.fileName}</div>
{f.lastModified && (
<div className="text-muted-foreground text-[10px]">
{new Date(f.lastModified).toLocaleString()}
</div>
)}
</div>
))
)}
</div>
</div>
)}
{checking && !isLoading && !status && (
<div className="text-xs text-muted-foreground mt-2">Không dữ liệu</div>
)}
</div>
);
}
const DeviceInfo = () => ( const DeviceInfo = () => (
<div className="space-y-3 min-w-[280px]"> <div className="space-y-3 min-w-[280px]">
<div> <div>
@ -69,6 +127,11 @@ export function ComputerCard({
</div> </div>
)} )}
<div>
<div className="text-xs text-muted-foreground mb-1">Kiểm tra thư mục</div>
<DeviceFolderCheck />
</div>
<div> <div>
<div className="text-xs text-muted-foreground mb-1">Trạng thái</div> <div className="text-xs text-muted-foreground mb-1">Trạng thái</div>
<Badge <Badge

View File

@ -53,6 +53,8 @@ export const API_ENDPOINTS = {
GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`, GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${commandId}`, UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${commandId}`,
DELETE_COMMAND: (commandId: number) => `${BASE_URL}/Command/delete/${commandId}`, DELETE_COMMAND: (commandId: number) => `${BASE_URL}/Command/delete/${commandId}`,
GET_SENSITIVE_COMMANDS: `${BASE_URL}/Command/sensitive`,
REQUEST_SEND_SENSITIVE_COMMAND: `${BASE_URL}/Command/send-sensitive`,
}, },
SSE_EVENTS: { SSE_EVENTS: {
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`, DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,

View File

@ -82,3 +82,47 @@ export function useDeleteCommand() {
}, },
}); });
} }
/**
* Hook đ lấy danh sách lệnh nhạy cảm
*/
export function useGetSensitiveCommands(enabled = true) {
return useQuery({
queryKey: [...COMMAND_QUERY_KEYS.all, "sensitive"],
queryFn: () => commandService.getSensitiveCommands(),
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
/**
* Hook đ gửi lệnh nhạy cảm
*/
export function useExecuteSensitiveCommand() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
roomName,
command,
password,
}: {
roomName: string;
command: any;
password: string;
}) =>
// API expects a SensitiveCommandRequest with PascalCase keys
commandService.requestSendSensitiveCommand({
Command: command,
Password: password,
RoomName: roomName,
}),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...COMMAND_QUERY_KEYS.all, "sensitive"],
});
},
});
}

View File

@ -171,3 +171,28 @@ export function useGetClientFolderStatus(roomName?: string, enabled = true) {
staleTime: 30 * 1000, staleTime: 30 * 1000,
}); });
} }
/**
* Hook to get folder status for a single device. The hook will fetch the
* folder status list for the device's room and return the matching entry
* for the provided `deviceId`.
*/
export function useGetClientFolderStatusForDevice(
deviceId?: string,
roomName?: string,
enabled = true
) {
return useQuery<ClientFolderStatus | undefined>({
queryKey: deviceId
? [...DEVICE_COMM_QUERY_KEYS.all, "folder-status-device", deviceId]
: ["disabled"],
queryFn: async () => {
if (!roomName) return Promise.reject("No room");
const list = await deviceCommService.getClientFolderStatus(roomName);
if (!Array.isArray(list)) return undefined;
return list.find((s: ClientFolderStatus) => s.deviceId === deviceId);
},
enabled: enabled && !!deviceId && !!roomName,
staleTime: 30 * 1000,
});
}

View File

@ -74,7 +74,6 @@ function AgentsPage() {
const columns: ColumnDef<Version>[] = [ const columns: ColumnDef<Version>[] = [
{ accessorKey: "version", header: "Phiên bản" }, { accessorKey: "version", header: "Phiên bản" },
{ accessorKey: "fileName", header: "Tên file" }, { accessorKey: "fileName", header: "Tên file" },
{ accessorKey: "folderPath", header: "Đường dẫn" },
{ {
accessorKey: "updatedAt", accessorKey: "updatedAt",
header: "Thời gian cập nhật", header: "Thời gian cập nhật",

View File

@ -55,7 +55,6 @@ function AppsComponent() {
const columns: ColumnDef<Version>[] = [ const columns: ColumnDef<Version>[] = [
{ accessorKey: "version", header: "Phiên bản" }, { accessorKey: "version", header: "Phiên bản" },
{ accessorKey: "fileName", header: "Tên file" }, { accessorKey: "fileName", header: "Tên file" },
{ accessorKey: "folderPath", header: "Đường dẫn" },
{ {
accessorKey: "updatedAt", accessorKey: "updatedAt",
header: () => <div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>, header: () => <div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>,

View File

@ -11,6 +11,7 @@ import {
useSendCommand, useSendCommand,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { Check, X, Edit2, Trash2 } from "lucide-react"; import { Check, X, Edit2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
@ -73,11 +74,19 @@ function CommandPage() {
accessorKey: "commandName", accessorKey: "commandName",
header: "Tên lệnh", header: "Tên lệnh",
size: 100, size: 100,
cell: ({ getValue }) => ( cell: ({ getValue, row }) => {
<div className="max-w-[100px]"> const full = (getValue() as string) || row.original.commandName || "";
<span className="font-semibold truncate block">{getValue() as string}</span> return (
</div> <div className="max-w-[100px]">
), <Tooltip>
<TooltipTrigger asChild>
<span className="font-semibold truncate block cursor-help">{full}</span>
</TooltipTrigger>
<TooltipContent side="bottom">{full}</TooltipContent>
</Tooltip>
</div>
);
},
}, },
{ {
accessorKey: "commandType", accessorKey: "commandType",
@ -93,18 +102,6 @@ function CommandPage() {
return <span>{typeMap[type] || "UNKNOWN"}</span>; return <span>{typeMap[type] || "UNKNOWN"}</span>;
}, },
}, },
{
accessorKey: "description",
header: "Mô tả",
size: 120,
cell: ({ getValue }) => (
<div className="max-w-[120px]">
<span className="text-sm text-muted-foreground truncate block">
{(getValue() as string) || "-"}
</span>
</div>
),
},
{ {
accessorKey: "commandContent", accessorKey: "commandContent",
header: "Nội dung lệnh", header: "Nội dung lệnh",
@ -153,7 +150,7 @@ function CommandPage() {
}, },
{ {
id: "select", id: "select",
header: () => <div className="text-center text-xs">Chọn đ thực thi</div>, header: () => <div className="text-center text-xs">Thực thi</div>,
cell: ({ row }) => ( cell: ({ row }) => (
<input <input
type="checkbox" type="checkbox"

View File

@ -80,7 +80,7 @@ function RoomDetailPage() {
Thực thi lệnh Thực thi lệnh
</div> </div>
<div className="flex items-center gap-3 flex-wrap justify-end"> <div className="flex items-center gap-3 justify-end">
{/* Command Action Buttons */} {/* Command Action Buttons */}
{devices.length > 0 && ( {devices.length > 0 && (
<> <>

View File

@ -51,3 +51,22 @@ export async function deleteCommand(commandId: number): Promise<any> {
); );
return response.data; return response.data;
} }
/**
* Lấy danh sách lệnh nhạy cảm
* @return - Danh sách lệnh nhạy cảm
* */
export async function getSensitiveCommands(): Promise<any[]> {
const response = await axios.get<any[]>(API_ENDPOINTS.COMMAND.GET_SENSITIVE_COMMANDS);
return response.data;
}
/**
* Gửi yêu cầu thực thi lệnh nhạy cảm
* @param data - Dữ liệu lệnh nhạy cảm
* @return - Kết quả thực thi
* */
export async function requestSendSensitiveCommand(data: any): Promise<any> {
const response = await axios.post(API_ENDPOINTS.COMMAND.REQUEST_SEND_SENSITIVE_COMMAND, data);
return response.data;
}

View File

@ -18,10 +18,10 @@ export const appSidebarSection = {
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"], versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
navMain: [ navMain: [
{ {
title: "Thống kê tổng quan", title: "Tổng quan",
items: [ items: [
{ {
title: "Dashboard", title: "Thống kê",
url: "/dashboard", url: "/dashboard",
code: AppSidebarSectionCode.DASHBOARD, code: AppSidebarSectionCode.DASHBOARD,
icon: Home, icon: Home,
@ -30,7 +30,7 @@ export const appSidebarSection = {
], ],
}, },
{ {
title: "Quan lý phòng máy", title: "Qun lý phòng máy",
items: [ items: [
{ {
title: "Danh sách phòng máy", title: "Danh sách phòng máy",
@ -42,17 +42,17 @@ export const appSidebarSection = {
], ],
}, },
{ {
title: "Agent và phần mềm", title: "Quản lý agent/thư mục Setup",
items: [ items: [
{ {
title: "Danh sách Agent", title: "Agent",
url: "/agent", url: "/agent",
code: AppSidebarSectionCode.AGENT_MANAGEMENT, code: AppSidebarSectionCode.AGENT_MANAGEMENT,
icon: AppWindow, icon: AppWindow,
permissions: [PermissionEnum.VIEW_AGENT], permissions: [PermissionEnum.VIEW_AGENT],
}, },
{ {
title: "Quản lý phần mềm", title: "Thư mục Setup",
url: "/apps", url: "/apps",
icon: AppWindow, icon: AppWindow,
permissions: [PermissionEnum.VIEW_APPS], permissions: [PermissionEnum.VIEW_APPS],
@ -60,7 +60,7 @@ export const appSidebarSection = {
], ],
}, },
{ {
title: "Lệnh và các ứng dụng bị chặn", title: "Quản lý lệnh/blacklist",
items: items:
[ [
{ {
@ -70,7 +70,7 @@ export const appSidebarSection = {
permissions: [PermissionEnum.VIEW_COMMAND], permissions: [PermissionEnum.VIEW_COMMAND],
}, },
{ {
title: "Danh sách ứng dụng/web bị chặn", title: "Chặn ứng dụng/website",
url: "/blacklists", url: "/blacklists",
icon: CircleX, icon: CircleX,
permissions: [PermissionEnum.ALLOW_ALL], permissions: [PermissionEnum.ALLOW_ALL],
@ -78,7 +78,7 @@ export const appSidebarSection = {
] ]
}, },
{ {
title: "Phân quyền và người dùng", title: "Quản lý tài khoản/phân quyền",
items: [ items: [
{ {
title: "Danh sách roles", title: "Danh sách roles",

View File

@ -15,4 +15,5 @@ export enum CommandType {
SHUTDOWN = 2, SHUTDOWN = 2,
TASKKILL = 3, TASKKILL = 3,
BLOCK = 4, BLOCK = 4,
RESET = 5,
} }

View File

@ -0,0 +1,9 @@
import type { CommandType } from "./command-registry";
export type SensitiveCommand = {
commandName: string;
commandType: CommandType;
commandContent: string;
qoS: 0 | 1 | 2;
isRetained: boolean;
}