Docs: document Users/Machines unification, company filter in Machines, Windows OS label, and identity/email/history guidance in OPERATIONS.md

This commit is contained in:
codex-bot 2025-10-21 10:55:07 -03:00
parent 231310a9fe
commit af0658af26
3 changed files with 95 additions and 0 deletions

View file

@ -22,6 +22,9 @@ import {
} from "@/components/ui/select"
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api"
import { useAuth } from "@/lib/auth-client"
import { ROLE_OPTIONS, type RoleOption } from "@/lib/authz"
import { canReactivateInvite as canReactivateInvitePolicy } from "@/lib/invite-policies"
@ -164,6 +167,7 @@ function isRestrictedRole(role?: string | null) {
}
export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, defaultTenantId, viewerRole }: Props) {
const { convexUserId } = useAuth()
const [users, setUsers] = useState<AdminUser[]>(initialUsers)
const [invites, setInvites] = useState<AdminInvite[]>(initialInvites)
const [companies, setCompanies] = useState<CompanyOption[]>([])
@ -274,6 +278,12 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
const [isCreatingUser, setIsCreatingUser] = useState(false)
const [createPassword, setCreatePassword] = useState<string | null>(null)
// Máquinas (para listar vínculos por usuário)
const machinesList = useQuery(
convexUserId ? api.machines.listByTenant : "skip",
convexUserId ? { tenantId: defaultTenantId, includeMetadata: true } : ("skip" as const)
) as Array<{ id: string; hostname?: string; assignedUserEmail?: string | null; metadata?: unknown }> | undefined
// Options of tenants present in dataset for filtering
const tenantOptions = useMemo(() => {
const set = new Set<string>()
@ -400,6 +410,28 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
})
setPasswordPreview(null)
}, [editUser, defaultTenantId])
const linkedMachinesForEditUser = useMemo(() => {
if (!editUser || !machinesList) return [] as Array<{ id: string; hostname?: string }>
const email = (editUser.email ?? "").toLowerCase()
const results: Array<{ id: string; hostname?: string }> = []
machinesList.forEach((m) => {
const assigned = (m.assignedUserEmail ?? "").toLowerCase()
let collaboratorEmail = ""
if (m.metadata && typeof m.metadata === "object") {
const rec = m.metadata as Record<string, unknown>
const c = rec["collaborator"]
if (c && typeof c === "object") {
const base = c as Record<string, unknown>
if (typeof base.email === "string") collaboratorEmail = base.email.toLowerCase()
}
}
if (assigned === email || (collaboratorEmail && collaboratorEmail === email)) {
results.push({ id: m.id, hostname: m.hostname })
}
})
return results
}, [editUser, machinesList])
useEffect(() => {
setCreateForm((prev) => ({
...prev,
@ -1783,6 +1815,23 @@ async function handleDeleteUser() {
</Select>
<p className="text-xs text-neutral-500">Essa seleção substitui o vínculo atual no portal do cliente.</p>
</div>
<div className="grid gap-2">
<Label>Máquinas vinculadas</Label>
{linkedMachinesForEditUser.length > 0 ? (
<ul className="divide-y divide-slate-200 rounded-md border border-slate-200 bg-slate-50/60">
{linkedMachinesForEditUser.map((m) => (
<li key={`linked-m-${m.id}`} className="flex items-center justify-between px-3 py-2 text-sm">
<span className="truncate">{m.hostname || m.id}</span>
<Button asChild size="sm" variant="ghost">
<Link href={`/admin/machines/${m.id}`}>Abrir</Link>
</Button>
</li>
))}
</ul>
) : (
<p className="text-xs text-neutral-500">Nenhuma máquina vinculada a este usuário.</p>
)}
</div>
{isMachineEditing ? (
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-3 text-sm text-neutral-600">
Os ajustes detalhados de agentes de máquina são feitos em <Link href="/admin/machines" className="underline underline-offset-4">Admin Máquinas</Link>.

View file

@ -3200,6 +3200,14 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
<Monitor className="size-4 text-slate-500" /> {machine.hostname}
</CardTitle>
<CardDescription className="line-clamp-1 text-xs">{machine.authEmail ?? "—"}</CardDescription>
{collaborator?.email ? (
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] text-slate-600">
<span className="rounded-full border border-slate-300 bg-slate-100 px-2 py-0.5">
Usuário vinculado: {collaborator.name ? `${collaborator.name} · ` : ""}{collaborator.email}
</span>
<Link href="/admin" className="underline underline-offset-4">gerenciar usuários</Link>
</div>
) : null}
{!isActive ? (
<Badge variant="outline" className="mt-2 w-fit border-rose-200 bg-rose-50 text-[10px] font-semibold uppercase tracking-wide text-rose-700">
Desativada