change quick command button in room detail page
This commit is contained in:
parent
67f5dbbb08
commit
dc7ed4c71a
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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 có 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 có 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
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: "Quản 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",
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ export enum CommandType {
|
||||||
SHUTDOWN = 2,
|
SHUTDOWN = 2,
|
||||||
TASKKILL = 3,
|
TASKKILL = 3,
|
||||||
BLOCK = 4,
|
BLOCK = 4,
|
||||||
|
RESET = 5,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
src/types/sensitive-command.ts
Normal file
9
src/types/sensitive-command.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user