change in room

This commit is contained in:
Do Manh Phuong 2025-10-06 15:52:48 +07:00
parent 26bb177d54
commit 1781b7cd2e
8 changed files with 445 additions and 20 deletions

129
package-lock.json generated
View File

@ -7,10 +7,12 @@
"name": ".",
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
@ -1319,6 +1321,12 @@
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
@ -1375,6 +1383,66 @@
}
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@ -1816,6 +1884,67 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",

View File

@ -11,10 +11,12 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",

View File

@ -0,0 +1,151 @@
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Checkbox } from "@/components/ui/checkbox"
import { Play, PlayCircle } from "lucide-react"
import { useState } from "react"
interface PresetCommand {
id: string
label: string
command: string
description?: string
}
interface PresetCommandsProps {
onSelectCommand: (command: string) => void
onExecuteMultiple?: (commands: string[]) => void
disabled?: boolean
}
// Danh sách các command có sẵn
const PRESET_COMMANDS: PresetCommand[] = [
{
id: "check-disk",
label: "Kiểm tra dung lượng ổ đĩa",
command: "df -h",
description: "Hiển thị thông tin dung lượng các ổ đĩa",
},
{
id: "check-memory",
label: "Kiểm tra RAM",
command: "free -h",
description: "Hiển thị thông tin bộ nhớ RAM",
},
{
id: "check-cpu",
label: "Kiểm tra CPU",
command: "top -bn1 | head -20",
description: "Hiển thị thông tin CPU và tiến trình",
},
{
id: "list-processes",
label: "Danh sách tiến trình",
command: "ps aux",
description: "Liệt kê tất cả tiến trình đang chạy",
},
{
id: "network-info",
label: "Thông tin mạng",
command: "ifconfig",
description: "Hiển thị cấu hình mạng",
},
{
id: "system-info",
label: "Thông tin hệ thống",
command: "uname -a",
description: "Hiển thị thông tin hệ điều hành",
},
{
id: "uptime",
label: "Thời gian hoạt động",
command: "uptime",
description: "Hiển thị thời gian hệ thống đã chạy",
},
{
id: "reboot",
label: "Khởi động lại",
command: "reboot",
description: "Khởi động lại thiết bị",
},
]
export function PresetCommands({ onSelectCommand, onExecuteMultiple, disabled }: PresetCommandsProps) {
const [selectedCommands, setSelectedCommands] = useState<Set<string>>(new Set())
const handleToggleCommand = (commandId: string) => {
setSelectedCommands((prev) => {
const newSet = new Set(prev)
if (newSet.has(commandId)) {
newSet.delete(commandId)
} else {
newSet.add(commandId)
}
return newSet
})
}
const handleExecuteSelected = () => {
const commands = PRESET_COMMANDS.filter((cmd) => selectedCommands.has(cmd.id)).map((cmd) => cmd.command)
if (commands.length > 0 && onExecuteMultiple) {
onExecuteMultiple(commands)
setSelectedCommands(new Set()) // Clear selection after execution
}
}
const handleSelectAll = () => {
if (selectedCommands.size === PRESET_COMMANDS.length) {
setSelectedCommands(new Set())
} else {
setSelectedCommands(new Set(PRESET_COMMANDS.map((cmd) => cmd.id)))
}
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-2">
<Button variant="outline" size="sm" onClick={handleSelectAll} disabled={disabled}>
<Checkbox checked={selectedCommands.size === PRESET_COMMANDS.length} className="mr-2" />
{selectedCommands.size === PRESET_COMMANDS.length ? "Bỏ chọn tất cả" : "Chọn tất cả"}
</Button>
{selectedCommands.size > 0 && (
<Button size="sm" onClick={handleExecuteSelected} disabled={disabled}>
<PlayCircle className="h-4 w-4 mr-2" />
Thực thi {selectedCommands.size} lệnh
</Button>
)}
</div>
<ScrollArea className="h-[25vh] w-full rounded-md border p-4">
<div className="space-y-2">
{PRESET_COMMANDS.map((preset) => (
<div
key={preset.id}
className="flex items-start gap-3 rounded-lg border p-3 hover:bg-accent transition-colors"
>
<Checkbox
checked={selectedCommands.has(preset.id)}
onCheckedChange={() => handleToggleCommand(preset.id)}
disabled={disabled}
className="mt-1"
/>
<div className="flex-1 space-y-1">
<div className="font-medium text-sm">{preset.label}</div>
{preset.description && <div className="text-xs text-muted-foreground">{preset.description}</div>}
<code className="text-xs bg-muted px-2 py-1 rounded block mt-1">{preset.command}</code>
</div>
<Button
size="sm"
variant="outline"
onClick={() => onSelectCommand(preset.command)}
disabled={disabled}
className="shrink-0"
>
<Play className="h-4 w-4" />
</Button>
</div>
))}
</div>
</ScrollArea>
</div>
)
}

View File

@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,56 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@ -14,6 +14,7 @@ import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index'
import { Route as AuthenticatedCommandIndexRouteImport } from './routes/_authenticated/command/index'
import { Route as AuthenticatedBlacklistIndexRouteImport } from './routes/_authenticated/blacklist/index'
import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index'
import { Route as AuthenticatedAgentIndexRouteImport } from './routes/_authenticated/agent/index'
import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index'
@ -43,6 +44,12 @@ const AuthenticatedCommandIndexRoute =
path: '/command/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedBlacklistIndexRoute =
AuthenticatedBlacklistIndexRouteImport.update({
id: '/blacklist/',
path: '/blacklist/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
id: '/apps/',
path: '/apps/',
@ -70,6 +77,7 @@ export interface FileRoutesByFullPath {
'/login': typeof AuthLoginIndexRoute
'/agent': typeof AuthenticatedAgentIndexRoute
'/apps': typeof AuthenticatedAppsIndexRoute
'/blacklist': typeof AuthenticatedBlacklistIndexRoute
'/command': typeof AuthenticatedCommandIndexRoute
'/room': typeof AuthenticatedRoomIndexRoute
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
@ -79,6 +87,7 @@ export interface FileRoutesByTo {
'/login': typeof AuthLoginIndexRoute
'/agent': typeof AuthenticatedAgentIndexRoute
'/apps': typeof AuthenticatedAppsIndexRoute
'/blacklist': typeof AuthenticatedBlacklistIndexRoute
'/command': typeof AuthenticatedCommandIndexRoute
'/room': typeof AuthenticatedRoomIndexRoute
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
@ -91,6 +100,7 @@ export interface FileRoutesById {
'/_auth/login/': typeof AuthLoginIndexRoute
'/_authenticated/agent/': typeof AuthenticatedAgentIndexRoute
'/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
'/_authenticated/blacklist/': typeof AuthenticatedBlacklistIndexRoute
'/_authenticated/command/': typeof AuthenticatedCommandIndexRoute
'/_authenticated/room/': typeof AuthenticatedRoomIndexRoute
'/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute
@ -102,6 +112,7 @@ export interface FileRouteTypes {
| '/login'
| '/agent'
| '/apps'
| '/blacklist'
| '/command'
| '/room'
| '/room/$roomName'
@ -111,6 +122,7 @@ export interface FileRouteTypes {
| '/login'
| '/agent'
| '/apps'
| '/blacklist'
| '/command'
| '/room'
| '/room/$roomName'
@ -122,6 +134,7 @@ export interface FileRouteTypes {
| '/_auth/login/'
| '/_authenticated/agent/'
| '/_authenticated/apps/'
| '/_authenticated/blacklist/'
| '/_authenticated/command/'
| '/_authenticated/room/'
| '/_authenticated/room/$roomName/'
@ -170,6 +183,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedCommandIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/blacklist/': {
id: '/_authenticated/blacklist/'
path: '/blacklist'
fullPath: '/blacklist'
preLoaderRoute: typeof AuthenticatedBlacklistIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/apps/': {
id: '/_authenticated/apps/'
path: '/apps'
@ -214,6 +234,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
interface AuthenticatedRouteChildren {
AuthenticatedAgentIndexRoute: typeof AuthenticatedAgentIndexRoute
AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
AuthenticatedBlacklistIndexRoute: typeof AuthenticatedBlacklistIndexRoute
AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute
AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
@ -222,6 +243,7 @@ interface AuthenticatedRouteChildren {
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedAgentIndexRoute: AuthenticatedAgentIndexRoute,
AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
AuthenticatedBlacklistIndexRoute: AuthenticatedBlacklistIndexRoute,
AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute,
AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/blacklist/')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_authenticated/blacklist/"!</div>
}

View File

@ -22,7 +22,6 @@ import {
ChevronLeft,
ChevronRight,
Clock,
Hash,
Loader2,
MapPin,
Monitor,
@ -65,20 +64,6 @@ function RoomDetailComponent() {
</div>
),
},
{
header: () => (
<div className="flex items-center gap-2">
<Hash className="h-4 w-4" />
MAC Address
</div>
),
accessorKey: "macAddress",
cell: ({ getValue }) => (
<code className="bg-muted px-2 py-1 rounded text-sm font-mono">
{getValue() as string}
</code>
),
},
{
header: () => (
<div className="flex items-center gap-2">
@ -114,16 +99,57 @@ function RoomDetailComponent() {
header: () => (
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
Đa chỉ IP
Phòng
</div>
),
accessorKey: "ipAddress",
accessorKey: "room",
cell: ({ getValue }) => (
<code className="bg-muted px-2 py-1 rounded text-sm font-mono">
{getValue() as string}
</code>
<span className="text-sm font-medium">{getValue() as string}</span>
),
},
{
header: () => (
<div className="flex items-center gap-2">
<Wifi className="h-4 w-4" />
Thông tin mạng
</div>
),
accessorKey: "networkInfos",
cell: ({ getValue }) => {
const networkInfos = getValue() as {
macAddress?: string;
ipAddress?: string;
}[];
if (!networkInfos || networkInfos.length === 0) {
return (
<span className="text-muted-foreground text-sm">
Không dữ liệu
</span>
);
}
return (
<div className="flex flex-col gap-1">
{networkInfos.map((info, idx) => (
<div
key={idx}
className="flex items-center gap-2 text-sm font-mono px-2 py-1 rounded bg-muted/30"
>
<span className="text-primary"></span>
<code className="bg-background px-2 py-0.5 rounded">
{info.macAddress ?? "-"}
</code>
<span className="text-muted-foreground"></span>
<code className="bg-background px-2 py-0.5 rounded">
{info.ipAddress ?? "-"}
</code>
</div>
))}
</div>
);
},
},
{
header: "Trạng thái",
accessorKey: "isOffline",