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:
parent
231310a9fe
commit
af0658af26
3 changed files with 95 additions and 0 deletions
|
|
@ -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>.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue