change in room
This commit is contained in:
parent
26bb177d54
commit
1781b7cd2e
129
package-lock.json
generated
129
package-lock.json
generated
|
@ -7,10 +7,12 @@
|
||||||
"name": ".",
|
"name": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@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-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
@ -1319,6 +1321,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
|
||||||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
|
"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": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
"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==",
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||||
|
|
|
@ -11,10 +11,12 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@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-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
|
151
src/components/preset-command.tsx
Normal file
151
src/components/preset-command.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal 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 }
|
56
src/components/ui/scroll-area.tsx
Normal file
56
src/components/ui/scroll-area.tsx
Normal 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 }
|
|
@ -14,6 +14,7 @@ import { Route as AuthRouteImport } from './routes/_auth'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index'
|
import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index'
|
||||||
import { Route as AuthenticatedCommandIndexRouteImport } from './routes/_authenticated/command/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 AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index'
|
||||||
import { Route as AuthenticatedAgentIndexRouteImport } from './routes/_authenticated/agent/index'
|
import { Route as AuthenticatedAgentIndexRouteImport } from './routes/_authenticated/agent/index'
|
||||||
import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index'
|
import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index'
|
||||||
|
@ -43,6 +44,12 @@ const AuthenticatedCommandIndexRoute =
|
||||||
path: '/command/',
|
path: '/command/',
|
||||||
getParentRoute: () => AuthenticatedRoute,
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthenticatedBlacklistIndexRoute =
|
||||||
|
AuthenticatedBlacklistIndexRouteImport.update({
|
||||||
|
id: '/blacklist/',
|
||||||
|
path: '/blacklist/',
|
||||||
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
|
} as any)
|
||||||
const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
|
const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
|
||||||
id: '/apps/',
|
id: '/apps/',
|
||||||
path: '/apps/',
|
path: '/apps/',
|
||||||
|
@ -70,6 +77,7 @@ export interface FileRoutesByFullPath {
|
||||||
'/login': typeof AuthLoginIndexRoute
|
'/login': typeof AuthLoginIndexRoute
|
||||||
'/agent': typeof AuthenticatedAgentIndexRoute
|
'/agent': typeof AuthenticatedAgentIndexRoute
|
||||||
'/apps': typeof AuthenticatedAppsIndexRoute
|
'/apps': typeof AuthenticatedAppsIndexRoute
|
||||||
|
'/blacklist': typeof AuthenticatedBlacklistIndexRoute
|
||||||
'/command': typeof AuthenticatedCommandIndexRoute
|
'/command': typeof AuthenticatedCommandIndexRoute
|
||||||
'/room': typeof AuthenticatedRoomIndexRoute
|
'/room': typeof AuthenticatedRoomIndexRoute
|
||||||
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
|
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
|
||||||
|
@ -79,6 +87,7 @@ export interface FileRoutesByTo {
|
||||||
'/login': typeof AuthLoginIndexRoute
|
'/login': typeof AuthLoginIndexRoute
|
||||||
'/agent': typeof AuthenticatedAgentIndexRoute
|
'/agent': typeof AuthenticatedAgentIndexRoute
|
||||||
'/apps': typeof AuthenticatedAppsIndexRoute
|
'/apps': typeof AuthenticatedAppsIndexRoute
|
||||||
|
'/blacklist': typeof AuthenticatedBlacklistIndexRoute
|
||||||
'/command': typeof AuthenticatedCommandIndexRoute
|
'/command': typeof AuthenticatedCommandIndexRoute
|
||||||
'/room': typeof AuthenticatedRoomIndexRoute
|
'/room': typeof AuthenticatedRoomIndexRoute
|
||||||
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
|
'/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
|
||||||
|
@ -91,6 +100,7 @@ export interface FileRoutesById {
|
||||||
'/_auth/login/': typeof AuthLoginIndexRoute
|
'/_auth/login/': typeof AuthLoginIndexRoute
|
||||||
'/_authenticated/agent/': typeof AuthenticatedAgentIndexRoute
|
'/_authenticated/agent/': typeof AuthenticatedAgentIndexRoute
|
||||||
'/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
|
'/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
|
||||||
|
'/_authenticated/blacklist/': typeof AuthenticatedBlacklistIndexRoute
|
||||||
'/_authenticated/command/': typeof AuthenticatedCommandIndexRoute
|
'/_authenticated/command/': typeof AuthenticatedCommandIndexRoute
|
||||||
'/_authenticated/room/': typeof AuthenticatedRoomIndexRoute
|
'/_authenticated/room/': typeof AuthenticatedRoomIndexRoute
|
||||||
'/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute
|
'/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute
|
||||||
|
@ -102,6 +112,7 @@ export interface FileRouteTypes {
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/agent'
|
| '/agent'
|
||||||
| '/apps'
|
| '/apps'
|
||||||
|
| '/blacklist'
|
||||||
| '/command'
|
| '/command'
|
||||||
| '/room'
|
| '/room'
|
||||||
| '/room/$roomName'
|
| '/room/$roomName'
|
||||||
|
@ -111,6 +122,7 @@ export interface FileRouteTypes {
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/agent'
|
| '/agent'
|
||||||
| '/apps'
|
| '/apps'
|
||||||
|
| '/blacklist'
|
||||||
| '/command'
|
| '/command'
|
||||||
| '/room'
|
| '/room'
|
||||||
| '/room/$roomName'
|
| '/room/$roomName'
|
||||||
|
@ -122,6 +134,7 @@ export interface FileRouteTypes {
|
||||||
| '/_auth/login/'
|
| '/_auth/login/'
|
||||||
| '/_authenticated/agent/'
|
| '/_authenticated/agent/'
|
||||||
| '/_authenticated/apps/'
|
| '/_authenticated/apps/'
|
||||||
|
| '/_authenticated/blacklist/'
|
||||||
| '/_authenticated/command/'
|
| '/_authenticated/command/'
|
||||||
| '/_authenticated/room/'
|
| '/_authenticated/room/'
|
||||||
| '/_authenticated/room/$roomName/'
|
| '/_authenticated/room/$roomName/'
|
||||||
|
@ -170,6 +183,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AuthenticatedCommandIndexRouteImport
|
preLoaderRoute: typeof AuthenticatedCommandIndexRouteImport
|
||||||
parentRoute: typeof AuthenticatedRoute
|
parentRoute: typeof AuthenticatedRoute
|
||||||
}
|
}
|
||||||
|
'/_authenticated/blacklist/': {
|
||||||
|
id: '/_authenticated/blacklist/'
|
||||||
|
path: '/blacklist'
|
||||||
|
fullPath: '/blacklist'
|
||||||
|
preLoaderRoute: typeof AuthenticatedBlacklistIndexRouteImport
|
||||||
|
parentRoute: typeof AuthenticatedRoute
|
||||||
|
}
|
||||||
'/_authenticated/apps/': {
|
'/_authenticated/apps/': {
|
||||||
id: '/_authenticated/apps/'
|
id: '/_authenticated/apps/'
|
||||||
path: '/apps'
|
path: '/apps'
|
||||||
|
@ -214,6 +234,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||||
interface AuthenticatedRouteChildren {
|
interface AuthenticatedRouteChildren {
|
||||||
AuthenticatedAgentIndexRoute: typeof AuthenticatedAgentIndexRoute
|
AuthenticatedAgentIndexRoute: typeof AuthenticatedAgentIndexRoute
|
||||||
AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
|
AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
|
||||||
|
AuthenticatedBlacklistIndexRoute: typeof AuthenticatedBlacklistIndexRoute
|
||||||
AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute
|
AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute
|
||||||
AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
|
AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
|
||||||
AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
|
AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
|
||||||
|
@ -222,6 +243,7 @@ interface AuthenticatedRouteChildren {
|
||||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||||
AuthenticatedAgentIndexRoute: AuthenticatedAgentIndexRoute,
|
AuthenticatedAgentIndexRoute: AuthenticatedAgentIndexRoute,
|
||||||
AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
|
AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
|
||||||
|
AuthenticatedBlacklistIndexRoute: AuthenticatedBlacklistIndexRoute,
|
||||||
AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute,
|
AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute,
|
||||||
AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
|
AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
|
||||||
AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,
|
AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,
|
||||||
|
|
9
src/routes/_authenticated/blacklist/index.tsx
Normal file
9
src/routes/_authenticated/blacklist/index.tsx
Normal 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>
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Clock,
|
Clock,
|
||||||
Hash,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
MapPin,
|
MapPin,
|
||||||
Monitor,
|
Monitor,
|
||||||
|
@ -65,20 +64,6 @@ function RoomDetailComponent() {
|
||||||
</div>
|
</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: () => (
|
header: () => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -114,16 +99,57 @@ function RoomDetailComponent() {
|
||||||
header: () => (
|
header: () => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MapPin className="h-4 w-4" />
|
<MapPin className="h-4 w-4" />
|
||||||
Địa chỉ IP
|
Phòng
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
accessorKey: "ipAddress",
|
accessorKey: "room",
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<code className="bg-muted px-2 py-1 rounded text-sm font-mono">
|
<span className="text-sm font-medium">{getValue() as string}</span>
|
||||||
{getValue() as string}
|
|
||||||
</code>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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 có 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",
|
header: "Trạng thái",
|
||||||
accessorKey: "isOffline",
|
accessorKey: "isOffline",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user