chore: prep platform improvements
This commit is contained in:
parent
a62f3d5283
commit
c5ddd54a3e
24 changed files with 777 additions and 649 deletions
|
|
@ -42,7 +42,7 @@ import {
|
|||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
|
|
@ -468,67 +468,6 @@ export function normalizeDeviceRemoteAccessList(raw: unknown): DeviceRemoteAcces
|
|||
return entries
|
||||
}
|
||||
|
||||
const REMOTE_ACCESS_METADATA_IGNORED_KEYS = new Set([
|
||||
"provider",
|
||||
"tool",
|
||||
"vendor",
|
||||
"name",
|
||||
"identifier",
|
||||
"code",
|
||||
"id",
|
||||
"accessId",
|
||||
"username",
|
||||
"user",
|
||||
"login",
|
||||
"email",
|
||||
"account",
|
||||
"password",
|
||||
"pass",
|
||||
"secret",
|
||||
"pin",
|
||||
"url",
|
||||
"link",
|
||||
"remoteUrl",
|
||||
"console",
|
||||
"viewer",
|
||||
"notes",
|
||||
"note",
|
||||
"description",
|
||||
"obs",
|
||||
"lastVerifiedAt",
|
||||
"verifiedAt",
|
||||
"checkedAt",
|
||||
"updatedAt",
|
||||
])
|
||||
|
||||
function extractRemoteAccessMetadataEntries(metadata: Record<string, unknown> | null | undefined) {
|
||||
if (!metadata) return [] as Array<[string, unknown]>
|
||||
return Object.entries(metadata).filter(([key, value]) => {
|
||||
if (REMOTE_ACCESS_METADATA_IGNORED_KEYS.has(key)) return false
|
||||
if (value === null || value === undefined) return false
|
||||
if (typeof value === "string" && value.trim().length === 0) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function formatRemoteAccessMetadataKey(key: string) {
|
||||
return key
|
||||
.replace(/[_.-]+/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
|
||||
function formatRemoteAccessMetadataValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return ""
|
||||
if (typeof value === "string") return value
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
if (value instanceof Date) return formatAbsoluteDateTime(value)
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function readText(record: Record<string, unknown>, ...keys: string[]): string | undefined {
|
||||
const stringValue = readString(record, ...keys)
|
||||
if (stringValue) return stringValue
|
||||
|
|
@ -3029,7 +2968,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [accessDialog, setAccessDialog] = useState(false)
|
||||
const [accessEmail, setAccessEmail] = useState<string>(primaryLinkedUser?.email ?? "")
|
||||
const [accessEmail, setAccessEmail] = useState<string>("")
|
||||
const [accessName, setAccessName] = useState<string>(primaryLinkedUser?.name ?? "")
|
||||
const [accessRole, setAccessRole] = useState<"collaborator" | "manager">(personaRole === "manager" ? "manager" : "collaborator")
|
||||
const [savingAccess, setSavingAccess] = useState(false)
|
||||
|
|
@ -3091,10 +3030,13 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
|
||||
// removed copy/export inventory JSON buttons as requested
|
||||
useEffect(() => {
|
||||
setAccessEmail(primaryLinkedUser?.email ?? "")
|
||||
setAccessEmail("")
|
||||
}, [device?.id])
|
||||
|
||||
useEffect(() => {
|
||||
setAccessName(primaryLinkedUser?.name ?? "")
|
||||
setAccessRole(personaRole === "manager" ? "manager" : "collaborator")
|
||||
}, [device?.id, primaryLinkedUser?.email, primaryLinkedUser?.name, personaRole])
|
||||
}, [device?.id, primaryLinkedUser?.name, personaRole])
|
||||
|
||||
useEffect(() => {
|
||||
setIsActiveLocal(device?.isActive ?? true)
|
||||
|
|
@ -3711,10 +3653,55 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
<InfoChip key={chip.key} label={chip.label} value={chip.value} icon={chip.icon} tone={chip.tone} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
|
||||
<ShieldCheck className="size-4" />
|
||||
Ajustar acesso
|
||||
</Button>
|
||||
{!isManualMobile ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2 border-dashed border-amber-300 text-amber-700 hover:border-amber-400 hover:text-amber-800"
|
||||
onClick={handleResetAgent}
|
||||
disabled={isResettingAgent}
|
||||
>
|
||||
<RefreshCcw className={cn("size-4", isResettingAgent && "animate-spin")} />
|
||||
{isResettingAgent ? "Resetando agente..." : "Resetar agente"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isActiveLocal ? "outline" : "default"}
|
||||
className={cn(
|
||||
"gap-2 border-dashed",
|
||||
!isActiveLocal && "bg-emerald-600 text-white hover:bg-emerald-600/90"
|
||||
)}
|
||||
onClick={handleToggleActive}
|
||||
disabled={togglingActive}
|
||||
>
|
||||
{isActiveLocal ? <Power className="size-4" /> : <PlayCircle className="size-4" />}
|
||||
{isActiveLocal ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Campos personalizados (posicionado logo após métricas) */}
|
||||
<div className="space-y-3">
|
||||
<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">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold text-neutral-900">Campos personalizados</h4>
|
||||
<Badge variant="outline" className="rounded-full px-2.5 py-0.5 text-[11px] font-semibold">
|
||||
|
|
@ -3816,50 +3803,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
|
||||
<ShieldCheck className="size-4" />
|
||||
Ajustar acesso
|
||||
</Button>
|
||||
{!isManualMobile ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-2 border-dashed border-amber-300 text-amber-700 hover:border-amber-400 hover:text-amber-800"
|
||||
onClick={handleResetAgent}
|
||||
disabled={isResettingAgent}
|
||||
>
|
||||
<RefreshCcw className={cn("size-4", isResettingAgent && "animate-spin")} />
|
||||
{isResettingAgent ? "Resetando agente..." : "Resetar agente"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isActiveLocal ? "outline" : "default"}
|
||||
className={cn(
|
||||
"gap-2 border-dashed",
|
||||
!isActiveLocal && "bg-emerald-600 text-white hover:bg-emerald-600/90"
|
||||
)}
|
||||
onClick={handleToggleActive}
|
||||
disabled={togglingActive}
|
||||
>
|
||||
{isActiveLocal ? <Power className="size-4" /> : <PlayCircle className="size-4" />}
|
||||
{isActiveLocal ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{device.registeredBy ? (
|
||||
<span
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"gap-2 border-dashed border-slate-200 bg-background cursor-default select-text text-neutral-700 hover:bg-background hover:text-neutral-700 focus-visible:outline-none"
|
||||
)}
|
||||
>
|
||||
Registrada via {device.registeredBy}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
|
|
@ -3889,7 +3833,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
{hasRemoteAccess ? (
|
||||
<div className="space-y-3">
|
||||
{remoteAccessEntries.map((entry) => {
|
||||
const metadataEntries = extractRemoteAccessMetadataEntries(entry.metadata)
|
||||
const lastVerifiedDate =
|
||||
entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt)
|
||||
? new Date(entry.lastVerifiedAt)
|
||||
|
|
@ -3977,12 +3920,12 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
) : null}
|
||||
{isRustDesk && (entry.identifier || entry.password) ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-1 inline-flex items-center gap-2 bg-white/80 text-slate-800 hover:bg-white"
|
||||
className="mt-1 inline-flex items-center gap-2 border-[#00d6eb]/60 bg-white text-slate-800 shadow-sm transition-colors hover:border-[#00d6eb] hover:bg-[#00e8ff]/10 hover:text-slate-900 focus-visible:border-[#00d6eb] focus-visible:ring-[#00e8ff]/30"
|
||||
onClick={() => handleRustDeskConnect(entry)}
|
||||
>
|
||||
<MonitorSmartphone className="size-4" /> Conectar via RustDesk
|
||||
<MonitorSmartphone className="size-4 text-[#009bb1]" /> Conectar via RustDesk
|
||||
</Button>
|
||||
) : null}
|
||||
{entry.notes ? (
|
||||
|
|
@ -4020,21 +3963,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{metadataEntries.length ? (
|
||||
<details className="mt-3 rounded-lg border border-slate-200 bg-white/70 px-3 py-2 text-[11px] text-slate-600">
|
||||
<summary className="cursor-pointer font-semibold text-slate-700 outline-none transition-colors hover:text-slate-900">
|
||||
Metadados adicionais
|
||||
</summary>
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-2">
|
||||
{metadataEntries.map(([key, value]) => (
|
||||
<div key={`${entry.clientId}-${key}`} className="flex items-center justify-between gap-3 rounded-md border border-slate-200 bg-white px-2 py-1 shadow-sm">
|
||||
<span className="font-semibold text-slate-700">{formatRemoteAccessMetadataKey(key)}</span>
|
||||
<span className="truncate text-right text-slate-600">{formatRemoteAccessMetadataValue(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
@ -4047,7 +3975,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-2">
|
||||
<section className="space-y-3 border-t border-slate-100 pt-6">
|
||||
<h4 className="text-sm font-semibold">Usuários vinculados</h4>
|
||||
<div className="space-y-2">
|
||||
{primaryLinkedUser?.email ? (
|
||||
|
|
@ -4339,7 +4267,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
</Dialog>
|
||||
|
||||
{!isManualMobile ? (
|
||||
<section className="space-y-2">
|
||||
<section className="space-y-3 border-t border-slate-100 pt-6">
|
||||
<h4 className="text-sm font-semibold">Sincronização</h4>
|
||||
<div className="grid gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex justify-between gap-4">
|
||||
|
|
@ -4377,7 +4305,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
) : null}
|
||||
|
||||
{!isManualMobile ? (
|
||||
<section className="space-y-2">
|
||||
<section className="space-y-3 border-t border-slate-100 pt-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">Métricas recentes</h4>
|
||||
{lastUpdateRelative ? (
|
||||
|
|
@ -4391,7 +4319,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
) : null}
|
||||
|
||||
{!isManualMobile && (hardware || network || (labels && labels.length > 0)) ? (
|
||||
<section className="space-y-3">
|
||||
<section className="space-y-4 border-t border-slate-100 pt-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">Inventário</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
|
@ -4500,7 +4428,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
|
||||
{/* Discos (agente) */}
|
||||
{disks.length > 0 ? (
|
||||
<section className="space-y-2">
|
||||
<section className="space-y-3 border-t border-slate-100 pt-6">
|
||||
<h4 className="text-sm font-semibold">Discos e partições</h4>
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50/60">
|
||||
<Table>
|
||||
|
|
@ -4531,7 +4459,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
|
||||
{/* Inventário estendido por SO */}
|
||||
{extended ? (
|
||||
<section className="space-y-3">
|
||||
<section className="space-y-4 border-t border-slate-100 pt-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">Inventário estendido</h4>
|
||||
<p className="text-xs text-muted-foreground">Dados ricos coletados pelo agente, variam por sistema operacional.</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue