Files
remotelink-docker/components/dashboard/machines-filter.tsx
monoadmin 61edbf59bf Add ScreenConnect-parity features (high + medium)
Viewer:
- Toolbar: Ctrl+Alt+Del, clipboard paste, monitor picker, file transfer, chat, WoL buttons
- Multi-monitor: agent sends monitor_list on connect, viewer can switch via dropdown
- Clipboard sync: agent polls local clipboard → sends to viewer; viewer paste → agent sets remote clipboard
- File transfer panel: drag-drop upload to agent, directory browser, download files from remote
- Chat panel: bidirectional text chat forwarded through relay

Agent:
- Multi-monitor capture with set_monitor/set_quality message handlers
- exec_key_combo for Ctrl+Alt+Del and arbitrary combos
- Clipboard polling via pyperclip (both directions)
- File upload/download/list_files with base64 chunked protocol
- Attended mode (--attended): zenity/kdialog/PowerShell consent dialog before accepting stream
- Auto-update: heartbeat checks version, downloads new binary and exec-replaces self (Linux)
- Reports MAC address on registration (for WoL)

Relay:
- Forwards monitor_list, clipboard_content, file_chunk, file_list, chat_message agent→viewer
- Session recording: when RECORDING_DIR env set, saves JPEG frames as .remrec files
- ALLOWED_ORIGINS CORS now set from NEXT_PUBLIC_APP_URL in docker-compose

Database:
- groups table (id, name, description, created_by)
- machines: group_id, mac_address, notes, tags text[]
- Migration 0003 applied

Dashboard:
- Machines page: search, tag filter, group filter, inline notes/tags/rename editing
- MachineCard: inline tag management, group picker, notes textarea
- Admin page: new Groups tab (create/list/delete groups)
- API: PATCH /api/machines/[id] (name, notes, tags, groupId)
- API: GET/POST/DELETE /api/groups
- API: POST /api/machines/wol (broadcast magic packet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 23:57:47 -07:00

109 lines
3.1 KiB
TypeScript

'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Search, X } from 'lucide-react'
import { useTransition } from 'react'
interface Group {
id: string
name: string
}
interface MachinesFilterProps {
allTags: string[]
groups: Group[]
currentSearch: string
currentTag: string
currentGroup: string
onlineCount: number
totalCount: number
}
export function MachinesFilter({
allTags,
groups,
currentSearch,
currentTag,
currentGroup,
onlineCount,
totalCount,
}: MachinesFilterProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [, startTransition] = useTransition()
const update = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString())
if (value) params.set(key, value)
else params.delete(key)
startTransition(() => router.push(`${pathname}?${params.toString()}`))
}
const clearAll = () => {
startTransition(() => router.push(pathname))
}
const hasFilters = currentSearch || currentTag || currentGroup
return (
<div className="space-y-3">
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search machines…"
defaultValue={currentSearch}
onChange={e => update('q', e.target.value)}
className="pl-8"
/>
</div>
{groups.length > 0 && (
<select
value={currentGroup}
onChange={e => update('group', e.target.value)}
className="rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="">All groups</option>
{groups.map(g => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground whitespace-nowrap">
<span className="text-green-500 font-medium">{onlineCount} online</span>
<span>/ {totalCount} total</span>
</div>
</div>
{/* Tag filter pills */}
{allTags.length > 0 && (
<div className="flex flex-wrap gap-1.5 items-center">
<span className="text-xs text-muted-foreground">Filter:</span>
{allTags.map(tag => (
<Badge
key={tag}
variant={currentTag === tag ? 'default' : 'outline'}
className="cursor-pointer text-xs h-5 py-0"
onClick={() => update('tag', currentTag === tag ? '' : tag)}
>
{tag}
</Badge>
))}
{hasFilters && (
<Button variant="ghost" size="sm" className="h-5 px-1.5 text-xs" onClick={clearAll}>
<X className="h-3 w-3 mr-1" />
Clear
</Button>
)}
</div>
)}
</div>
)
}