From 129407dbcea7203a8cf87bf732c45ddd3effd8ae Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Fri, 10 Oct 2025 10:26:35 -0300 Subject: [PATCH] feat(admin/machines): company search input with suggestions; rename machine dialog using Convex mutation; improve CPU name rendering and Defender booleans; add pulsating online indicator on cards and detail --- convex/machines.ts | 34 ++++ .../machines/admin-machines-overview.tsx | 151 ++++++++++++++---- 2 files changed, 151 insertions(+), 34 deletions(-) diff --git a/convex/machines.ts b/convex/machines.ts index 526dcca..36c5ba5 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -650,3 +650,37 @@ export const linkAuthAccount = mutation({ return { ok: true } }, }) + +export const rename = mutation({ + args: { + machineId: v.id("machines"), + actorId: v.id("users"), + tenantId: v.optional(v.string()), + hostname: v.string(), + }, + handler: async (ctx, { machineId, actorId, tenantId, hostname }) => { + // Reutiliza requireStaff através de tickets.ts helpers + const machine = await ctx.db.get(machineId) + if (!machine) { + throw new ConvexError("Máquina não encontrada") + } + // Verifica permissão no tenant da máquina + const viewer = await ctx.db.get(actorId) + if (!viewer || viewer.tenantId !== machine.tenantId) { + throw new ConvexError("Acesso negado ao tenant da máquina") + } + const normalizedRole = (viewer.role ?? "AGENT").toUpperCase() + const STAFF = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]) + if (!STAFF.has(normalizedRole)) { + throw new ConvexError("Apenas equipe interna pode renomear máquinas") + } + + const nextName = hostname.trim() + if (nextName.length < 2) { + throw new ConvexError("Informe um nome válido para a máquina") + } + + await ctx.db.patch(machineId, { hostname: nextName, updatedAt: Date.now() }) + return { ok: true } + }, +}) diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 71254c4..984e369 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -2,12 +2,14 @@ import { useEffect, useMemo, useState } from "react" import { useQuery } from "convex/react" +import { useMutation } from "convex/react" import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { toast } from "sonner" -import { ClipboardCopy, ServerCog, Cpu, MemoryStick, Monitor, HardDrive } from "lucide-react" +import { ClipboardCopy, ServerCog, Cpu, MemoryStick, Monitor, HardDrive, Pencil } from "lucide-react" import { api } from "@/convex/_generated/api" +import type { Id } from "@/convex/_generated/dataModel" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -26,6 +28,7 @@ import { import { Separator } from "@/components/ui/separator" import { cn } from "@/lib/utils" import Link from "next/link" +import { useAuth } from "@/lib/auth-client" type MachineMetrics = Record | null @@ -190,6 +193,12 @@ function formatPercent(value?: number | null) { return `${normalized.toFixed(0)}%` } +function fmtBool(value: unknown) { + if (value === true) return "Sim" + if (value === false) return "Não" + return "—" +} + function getStatusVariant(status?: string | null) { if (!status) return { label: statusLabels.unknown, className: statusClasses.unknown } const normalized = status.toLowerCase() @@ -204,7 +213,7 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { const [q, setQ] = useState("") const [statusFilter, setStatusFilter] = useState("all") const [osFilter, setOsFilter] = useState("all") - const [companyFilter, setCompanyFilter] = useState("all") + const [companyQuery, setCompanyQuery] = useState("") const [onlyAlerts, setOnlyAlerts] = useState(false) const osOptions = useMemo(() => { @@ -228,7 +237,10 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { if (s !== statusFilter) return false } if (osFilter !== "all" && (m.osName ?? "").toLowerCase() !== osFilter.toLowerCase()) return false - if (companyFilter !== "all" && (m.companySlug ?? "") !== companyFilter) return false + if (companyQuery && companyQuery.trim().length > 0) { + const slug = (m.companySlug ?? "").toLowerCase() + if (!slug.includes(companyQuery.trim().toLowerCase())) return false + } if (!text) return true const hay = [ m.hostname, @@ -276,22 +288,36 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { ))} - +
+ setCompanyQuery(e.target.value)} + placeholder="Buscar empresa (slug)" + className="min-w-[220px]" + /> + {companyQuery && companyOptions.filter((c) => c.toLowerCase().includes(companyQuery.toLowerCase())).slice(0,6).length > 0 ? ( +
+ {companyOptions + .filter((c) => c.toLowerCase().includes(companyQuery.toLowerCase())) + .slice(0, 8) + .map((c) => ( + + ))} +
+ ) : null} +
- + {machines.length === 0 ? ( @@ -328,6 +354,7 @@ type MachineDetailsProps = { } export function MachineDetails({ machine }: MachineDetailsProps) { + const { convexUserId } = useAuth() const metadata = machine?.inventory ?? null const metrics = machine?.metrics ?? null const hardware = metadata?.hardware ?? null @@ -371,6 +398,10 @@ export function MachineDetails({ machine }: MachineDetailsProps) { } } + const renameMachine = useMutation(api.machines.rename as any) + const [renaming, setRenaming] = useState(false) + const [newName, setNewName] = useState(machine?.hostname ?? "") + const [openDialog, setOpenDialog] = useState(false) const [dialogQuery, setDialogQuery] = useState("") const jsonText = useMemo(() => { @@ -451,7 +482,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
-

{machine.hostname}

+
+

{machine.hostname}

+ +

{machine.authEmail ?? "E-mail não definido"}

@@ -463,6 +500,12 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
+ {String(machine.status ?? "").toLowerCase() === "online" ? ( +
+ + +
+ ) : null}
{machine.osName ?? "SO desconhecido"} {machine.osVersion ?? ""} @@ -484,6 +527,41 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
+ {/* Renomear máquina */} + + + + Renomear máquina + +
+ setNewName(e.target.value)} placeholder="Novo hostname" /> +
+ + +
+
+
+
+

Sincronização

@@ -700,8 +778,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
-

CPU

-

{String((winCpu as any)?.Name ?? "—")}

+

CPU

+

{String((winCpu as any)?.Name ?? "—")}

@@ -739,7 +817,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { {Array.isArray(windowsExt.cpu) ? ( (windowsExt.cpu as Array>).slice(0,1).map((c, i) => (
- + @@ -891,8 +969,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) {

Defender

- - + +
) : null} @@ -1054,19 +1132,24 @@ function MachineCard({ machine }: { machine: MachinesQueryItem }) { return ( - +
+ + {String(machine.status ?? "").toLowerCase() === "online" ? ( + + ) : null} +
{machine.hostname}