Redesenho da UI de dispositivos e correcao de VRAM
- Reorganiza layout da tela de dispositivos admin - Renomeia secao "Controles do dispositivo" para "Atalhos" - Adiciona botao de Tickets com badge de quantidade - Simplifica textos de botoes (Acesso, Resetar) - Remove email da maquina do cabecalho - Move empresa e status para mesma linha - Remove chip de Build do resumo - Corrige deteccao de VRAM para GPUs >4GB usando nvidia-smi - Adiciona prefixo "VRAM" na exibicao de memoria da GPU - Documenta sincronizacao RustDesk 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c5150fee8f
commit
23e7cf58ae
11 changed files with 863 additions and 441 deletions
|
|
@ -41,6 +41,7 @@ import {
|
|||
Usb,
|
||||
Loader2,
|
||||
X,
|
||||
TicketCheck,
|
||||
} from "lucide-react"
|
||||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
|
@ -3154,14 +3155,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
icon: <Cpu className="size-4 text-neutral-500" />,
|
||||
})
|
||||
}
|
||||
if (windowsBuildLabel) {
|
||||
chips.push({
|
||||
key: "build",
|
||||
label: "Build",
|
||||
value: windowsBuildLabel,
|
||||
icon: <ServerCog className="size-4 text-neutral-500" />,
|
||||
})
|
||||
}
|
||||
if (windowsActivationStatus !== null && windowsActivationStatus !== undefined) {
|
||||
chips.push({
|
||||
key: "activation",
|
||||
|
|
@ -3222,7 +3215,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
device?.osVersion,
|
||||
device?.architecture,
|
||||
windowsVersionLabel,
|
||||
windowsBuildLabel,
|
||||
windowsActivationStatus,
|
||||
primaryLinkedUser?.email,
|
||||
primaryLinkedUser?.name,
|
||||
|
|
@ -3867,25 +3859,49 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
return (
|
||||
<Card className="border-slate-200">
|
||||
<CardHeader className="gap-1">
|
||||
<CardTitle>Detalhes</CardTitle>
|
||||
<CardDescription>Resumo do dispositivo selecionado</CardDescription>
|
||||
{device ? (
|
||||
<CardAction>
|
||||
<div className="flex flex-col items-end gap-2 text-xs sm:text-sm">
|
||||
{companyName ? (
|
||||
<div className="rounded-lg border border-slate-200 bg-white px-3 py-1 font-semibold text-neutral-600 shadow-sm">
|
||||
{companyName}
|
||||
</div>
|
||||
<>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="break-words text-2xl font-semibold text-neutral-900">
|
||||
{device.displayName ?? device.hostname ?? "Dispositivo"}
|
||||
</span>
|
||||
{isManualMobile ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Identificação interna
|
||||
</span>
|
||||
) : null}
|
||||
{!isDeactivated ? <DeviceStatusBadge status={effectiveStatus} /> : null}
|
||||
{!isActiveLocal ? (
|
||||
<Badge variant="outline" className="h-7 border-rose-200 bg-rose-50 px-3 font-semibold uppercase text-rose-700">
|
||||
Dispositivo desativada
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</CardAction>
|
||||
) : null}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
onClick={() => {
|
||||
setNewName(device.displayName ?? device.hostname ?? "")
|
||||
setRenaming(true)
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
<span className="sr-only">Renomear dispositivo</span>
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 text-xs sm:text-sm">
|
||||
{companyName ? (
|
||||
<div className="max-w-[200px] truncate rounded-lg border border-slate-200 bg-white px-3 py-1 font-semibold text-neutral-600 shadow-sm" title={companyName}>
|
||||
{companyName}
|
||||
</div>
|
||||
) : null}
|
||||
{!isDeactivated ? <DeviceStatusBadge status={effectiveStatus} /> : null}
|
||||
{!isActiveLocal ? (
|
||||
<Badge variant="outline" className="h-7 border-rose-200 bg-rose-50 px-3 font-semibold uppercase text-rose-700">
|
||||
Dispositivo desativado
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</CardAction>
|
||||
</>
|
||||
) : (
|
||||
<CardTitle>Detalhes</CardTitle>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{!device ? (
|
||||
|
|
@ -3893,47 +3909,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
) : (
|
||||
<div className="space-y-6">
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-start gap-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="break-words text-2xl font-semibold text-neutral-900">
|
||||
{device.displayName ?? device.hostname ?? "Dispositivo"}
|
||||
</h1>
|
||||
{isManualMobile ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Identificação interna
|
||||
</span>
|
||||
) : null}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
onClick={() => {
|
||||
setNewName(device.displayName ?? device.hostname ?? "")
|
||||
setRenaming(true)
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
<span className="sr-only">Renomear dispositivo</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{device.authEmail ?? "E-mail não definido"}</span>
|
||||
{device.authEmail ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyEmail}
|
||||
className="inline-flex items-center rounded-md p-1 text-neutral-500 transition hover:bg-[#00d6eb]/15 hover:text-[#0a4760] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00d6eb]/40 focus-visible:ring-offset-2"
|
||||
title="Copiar e-mail do dispositivo"
|
||||
aria-label="Copiar e-mail do dispositivo"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* ping integrado na badge de status */}
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{summaryChips.map((chip) => (
|
||||
<InfoChip
|
||||
|
|
@ -3975,17 +3950,32 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
|
||||
<div className="rounded-2xl border border-slate-200 bg-white/80 px-4 py-4 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Controles do dispositivo</p>
|
||||
{device.registeredBy ? (
|
||||
<span className="text-xs font-medium text-slate-500">
|
||||
Registrada via <span className="text-slate-800">{device.registeredBy}</span>
|
||||
</span>
|
||||
) : null}
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Atalhos</p>
|
||||
<span className="text-xs font-medium text-slate-500">
|
||||
{device.registeredBy === "desktop-agent" ? (
|
||||
<span className="text-slate-800">Agente na máquina</span>
|
||||
) : (
|
||||
<span className="text-slate-800">Manual</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{deviceTicketsHref ? (
|
||||
<Button size="sm" variant="outline" className="gap-2 border-dashed" asChild>
|
||||
<Link href={deviceTicketsHref}>
|
||||
<TicketCheck className="size-4" />
|
||||
Tickets
|
||||
{totalOpenTickets > 0 ? (
|
||||
<Badge variant="secondary" className="ml-1 rounded-full px-2 py-0 text-[10px] font-semibold">
|
||||
{totalOpenTickets}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
|
||||
<ShieldCheck className="size-4" />
|
||||
Ajustar acesso
|
||||
Acesso
|
||||
</Button>
|
||||
{!isManualMobile ? (
|
||||
<>
|
||||
|
|
@ -3997,7 +3987,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
disabled={isResettingAgent}
|
||||
>
|
||||
<RefreshCcw className={cn("size-4", isResettingAgent && "animate-spin")} />
|
||||
{isResettingAgent ? "Resetando agente..." : "Resetar agente"}
|
||||
{isResettingAgent ? "Resetando..." : "Resetar"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -4029,7 +4019,195 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campos personalizados (posicionado logo após métricas) */}
|
||||
{/* Acesso remoto */}
|
||||
<div className="space-y-3 border-t border-slate-100 pt-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">Acesso remoto</h4>
|
||||
{hasRemoteAccess ? (
|
||||
<Badge variant="outline" className="rounded-full px-2.5 py-0.5 text-[11px] font-semibold">
|
||||
{remoteAccessEntries.length === 1 ? "1 acesso" : `${remoteAccessEntries.length} acessos`}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
{canManageRemoteAccess ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2 border-dashed"
|
||||
onClick={() => {
|
||||
setEditingRemoteAccessClientId(null)
|
||||
setRemoteAccessDialog(true)
|
||||
}}
|
||||
>
|
||||
<Key className="size-4" />
|
||||
{hasRemoteAccess ? "Adicionar acesso" : "Cadastrar acesso"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{hasRemoteAccess ? (
|
||||
<div className="space-y-3">
|
||||
{remoteAccessEntries.map((entry) => {
|
||||
const lastVerifiedDate =
|
||||
entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt)
|
||||
? new Date(entry.lastVerifiedAt)
|
||||
: null
|
||||
const isRustDesk = isRustDeskAccess(entry)
|
||||
const secretVisible = Boolean(visibleRemoteSecrets[entry.clientId])
|
||||
return (
|
||||
<div key={entry.clientId} className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-xs sm:text-sm text-slate-700">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-slate-500">ID</span>
|
||||
{entry.identifier ? (
|
||||
<code className="rounded-md border border-slate-200 bg-white px-2.5 py-1 font-mono text-sm font-semibold text-neutral-800">
|
||||
{entry.identifier}
|
||||
</code>
|
||||
) : null}
|
||||
{entry.identifier ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 transition-colors hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => handleCopyRemoteIdentifier(entry.identifier)}
|
||||
title="Copiar ID"
|
||||
aria-label="Copiar ID"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{entry.username || entry.password ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
{entry.username ? (
|
||||
<div className="inline-flex flex-wrap items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Usuário</span>
|
||||
<code className="rounded-md border border-slate-200 bg-white px-2 py-0.5 font-mono text-xs text-slate-700">
|
||||
{entry.username}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => handleCopyRemoteCredential(entry.username, "Usuário do acesso remoto")}
|
||||
title="Copiar usuário"
|
||||
aria-label="Copiar usuário"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{entry.password ? (
|
||||
<div className="inline-flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-slate-500">Senha</span>
|
||||
<code className="rounded-md border border-slate-200 bg-white px-2.5 py-1 font-mono text-sm text-slate-700">
|
||||
{secretVisible ? entry.password : "••••••••"}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => toggleRemoteSecret(entry.clientId)}
|
||||
title={secretVisible ? "Ocultar senha" : "Mostrar senha"}
|
||||
aria-label={secretVisible ? "Ocultar senha" : "Mostrar senha"}
|
||||
>
|
||||
{secretVisible ? <EyeOff className="size-3.5" /> : <Eye className="size-3.5" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => handleCopyRemoteCredential(entry.password, "Senha do acesso remoto")}
|
||||
title="Copiar senha"
|
||||
aria-label="Copiar senha"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{entry.url && !isRustDesk ? (
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 text-slate-600 underline-offset-4 hover:text-slate-900 hover:underline"
|
||||
>
|
||||
Abrir console remoto
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 text-right">
|
||||
{entry.notes ? (
|
||||
<p className="whitespace-pre-wrap text-xs text-slate-600">{entry.notes}</p>
|
||||
) : null}
|
||||
{lastVerifiedDate ? (
|
||||
<p className="text-xs text-slate-500">
|
||||
Atualizado {formatRelativeTime(lastVerifiedDate)}{" "}
|
||||
<span className="text-slate-400">({formatAbsoluteDateTime(lastVerifiedDate)})</span>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-200 pt-3 mt-3">
|
||||
<div>
|
||||
{isRustDesk && (entry.identifier || entry.password) ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="inline-flex items-center gap-2 border-slate-300 bg-white text-slate-800 shadow-sm transition-colors hover:border-slate-400 hover:bg-slate-50 hover:text-slate-900 focus-visible:border-slate-400 focus-visible:ring-slate-200"
|
||||
onClick={() => handleRustDeskConnect(entry)}
|
||||
>
|
||||
<MonitorSmartphone className="size-4 text-[#4b5563]" /> Conectar via RustDesk
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{entry.provider ? (
|
||||
<Button variant="outline" size="sm" className="gap-2 border-slate-300 cursor-default hover:bg-white">
|
||||
{entry.provider}
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageRemoteAccess ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2 border-slate-300"
|
||||
onClick={() => {
|
||||
setEditingRemoteAccessClientId(entry.clientId)
|
||||
setRemoteAccessDialog(true)
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-3.5" /> Editar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="gap-2 text-rose-600 hover:border-rose-200 hover:bg-rose-50 hover:text-rose-700"
|
||||
onClick={() => void handleRemoveRemoteAccess(entry)}
|
||||
disabled={remoteAccessSaving}
|
||||
>
|
||||
<ShieldOff className="size-3.5" /> Remover
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-slate-200 bg-slate-50 px-4 py-3 text-xs sm:text-sm text-slate-600">
|
||||
Nenhum identificador de acesso remoto cadastrado. Registre o ID do TeamViewer, AnyDesk ou outra ferramenta para agilizar o suporte.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Campos personalizados */}
|
||||
<div className="space-y-3 border-t border-slate-100 pt-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
@ -4068,248 +4246,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-[color:var(--accent)] bg-[color:var(--accent)]/80 px-4 py-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm font-semibold text-accent-foreground">Tickets abertos por este dispositivo</h4>
|
||||
{totalOpenTickets === 0 ? (
|
||||
<p className="text-xs text-[color:var(--accent-foreground)]/80">
|
||||
Nenhum chamado em aberto registrado diretamente por este dispositivo.
|
||||
</p>
|
||||
) : hasAdditionalOpenTickets ? (
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.14em] text-[color:var(--accent-foreground)]/70">
|
||||
Mostrando últimos {Math.min(displayLimit, totalOpenTickets)} de {totalOpenTickets} chamados em aberto
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-[color:var(--accent-foreground)]/80">
|
||||
Últimos chamados vinculados a este dispositivo.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex h-10 min-w-[56px] items-center justify-center rounded-xl border border-[color:var(--accent)] bg-white px-3 text-[color:var(--accent-foreground)] shadow-sm">
|
||||
<span className="text-lg font-semibold leading-none tabular-nums">{totalOpenTickets}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{totalOpenTickets > 0 ? (
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{displayedDeviceTickets.map((ticket) => {
|
||||
const priorityMeta = getTicketPriorityMeta(ticket.priority)
|
||||
return (
|
||||
<Link
|
||||
key={ticket.id}
|
||||
href={`/tickets/${ticket.id}`}
|
||||
className="group flex h-full flex-col justify-between gap-3 rounded-xl border border-[color:var(--accent)] bg-white p-3 text-sm shadow-sm transition hover:-translate-y-0.5 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] focus-visible:ring-offset-2"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="line-clamp-2 font-medium text-neutral-900">
|
||||
#{ticket.reference} · {ticket.subject}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
Atualizado {formatRelativeTime(new Date(ticket.updatedAt))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold", priorityMeta.badgeClass)}>
|
||||
{priorityMeta.label}
|
||||
</Badge>
|
||||
<TicketStatusBadge status={ticket.status} className="h-7 px-3 text-xs font-semibold" />
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{deviceTicketsHref ? (
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
href={deviceTicketsHref}
|
||||
className="text-xs font-semibold text-[color:var(--accent-foreground)] underline-offset-4 transition hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-foreground)] focus-visible:ring-offset-2"
|
||||
>
|
||||
Ver todos
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-slate-100 pt-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">Acesso remoto</h4>
|
||||
{hasRemoteAccess ? (
|
||||
<Badge variant="outline" className="border-slate-200 bg-slate-100 text-[11px] font-semibold text-slate-700">
|
||||
{remoteAccessEntries.length === 1
|
||||
? remoteAccessEntries[0].provider ?? "Configuração única"
|
||||
: `${remoteAccessEntries.length} acessos`}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
{canManageRemoteAccess ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2 border-dashed"
|
||||
onClick={() => {
|
||||
setEditingRemoteAccessClientId(null)
|
||||
setRemoteAccessDialog(true)
|
||||
}}
|
||||
>
|
||||
<Key className="size-4" />
|
||||
{hasRemoteAccess ? "Adicionar acesso" : "Cadastrar acesso"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{hasRemoteAccess ? (
|
||||
<div className="space-y-3">
|
||||
{remoteAccessEntries.map((entry) => {
|
||||
const lastVerifiedDate =
|
||||
entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt)
|
||||
? new Date(entry.lastVerifiedAt)
|
||||
: null
|
||||
const isRustDesk = isRustDeskAccess(entry)
|
||||
const secretVisible = Boolean(visibleRemoteSecrets[entry.clientId])
|
||||
return (
|
||||
<div key={entry.clientId} className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-xs sm:text-sm text-slate-700">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{entry.provider ? (
|
||||
<Badge variant="outline" className="border-slate-200 bg-white text-[11px] font-semibold text-slate-700">
|
||||
{entry.provider}
|
||||
</Badge>
|
||||
) : null}
|
||||
{entry.identifier ? (
|
||||
<span className="font-semibold text-neutral-800">{entry.identifier}</span>
|
||||
) : null}
|
||||
{entry.identifier ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 transition-colors hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => handleCopyRemoteIdentifier(entry.identifier)}
|
||||
title="Copiar ID"
|
||||
aria-label="Copiar ID"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{entry.username || entry.password ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
{entry.username ? (
|
||||
<div className="inline-flex flex-wrap items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Usuário</span>
|
||||
<code className="rounded-md border border-slate-200 bg-white px-2 py-0.5 font-mono text-xs text-slate-700">
|
||||
{entry.username}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => handleCopyRemoteCredential(entry.username, "Usuário do acesso remoto")}
|
||||
title="Copiar usuário"
|
||||
aria-label="Copiar usuário"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{entry.password ? (
|
||||
<div className="inline-flex flex-wrap items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Senha</span>
|
||||
<code className="rounded-md border border-slate-200 bg-white px-2 py-0.5 font-mono text-xs text-slate-700">
|
||||
{secretVisible ? entry.password : "••••••••"}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => toggleRemoteSecret(entry.clientId)}
|
||||
>
|
||||
{secretVisible ? <EyeOff className="size-3.5" /> : <Eye className="size-3.5" />}
|
||||
{secretVisible ? "Ocultar" : "Mostrar"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
|
||||
onClick={() => handleCopyRemoteCredential(entry.password, "Senha do acesso remoto")}
|
||||
title="Copiar senha"
|
||||
aria-label="Copiar senha"
|
||||
>
|
||||
<ClipboardCopy className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{entry.url && !isRustDesk ? (
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 text-slate-600 underline-offset-4 hover:text-slate-900 hover:underline"
|
||||
>
|
||||
Abrir console remoto
|
||||
</a>
|
||||
) : null}
|
||||
{isRustDesk && (entry.identifier || entry.password) ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-1 inline-flex items-center gap-2 border-slate-300 bg-white text-slate-800 shadow-sm transition-colors hover:border-slate-400 hover:bg-slate-50 hover:text-slate-900 focus-visible:border-slate-400 focus-visible:ring-slate-200"
|
||||
onClick={() => handleRustDeskConnect(entry)}
|
||||
>
|
||||
<MonitorSmartphone className="size-4 text-[#4b5563]" /> Conectar via RustDesk
|
||||
</Button>
|
||||
) : null}
|
||||
{entry.notes ? (
|
||||
<p className="whitespace-pre-wrap text-[11px] text-slate-600">{entry.notes}</p>
|
||||
) : null}
|
||||
{lastVerifiedDate ? (
|
||||
<p className="text-[11px] text-slate-500">
|
||||
Atualizado {formatRelativeTime(lastVerifiedDate)}{" "}
|
||||
<span className="text-slate-400">({formatAbsoluteDateTime(lastVerifiedDate)})</span>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{canManageRemoteAccess ? (
|
||||
<div className="flex items-start gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2 border-slate-300"
|
||||
onClick={() => {
|
||||
setEditingRemoteAccessClientId(entry.clientId)
|
||||
setRemoteAccessDialog(true)
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-3.5" /> Editar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="gap-2 text-rose-600 hover:border-rose-200 hover:bg-rose-50 hover:text-rose-700"
|
||||
onClick={() => void handleRemoveRemoteAccess(entry)}
|
||||
disabled={remoteAccessSaving}
|
||||
>
|
||||
<ShieldOff className="size-3.5" /> Remover
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-slate-200 bg-slate-50 px-4 py-3 text-xs sm:text-sm text-slate-600">
|
||||
Nenhum identificador de acesso remoto cadastrado. Registre o ID do TeamViewer, AnyDesk ou outra ferramenta para agilizar o suporte.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3 border-t border-slate-100 pt-6">
|
||||
|
|
@ -4686,7 +4622,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
return (
|
||||
<li key={`gpu-${idx}`}>
|
||||
<span className="font-medium text-foreground">{name ?? "Adaptador de vídeo"}</span>
|
||||
{memoryBytes ? <span className="ml-1 text-muted-foreground">{formatBytes(memoryBytes)}</span> : null}
|
||||
{memoryBytes ? <span className="ml-1 text-muted-foreground">VRAM {formatBytes(memoryBytes)}</span> : null}
|
||||
{vendor ? <span className="ml-1 text-muted-foreground">· {vendor}</span> : null}
|
||||
{driver ? <span className="ml-1 text-muted-foreground">· Driver {driver}</span> : null}
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue