- Move USB policy control from bottom of page to modal dialog - Add "Controle USB" button in device controls section - Show USB chip for all Windows devices (default to ALLOW) - Add close button (X) with hover effect in modal header - Fix all Portuguese accents in USB control component - Position status badge at top of modal content - Add variant prop to UsbPolicyControl (card/inline) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { useMutation, useQuery } from "convex/react"
|
|
import { api } from "@/convex/_generated/api"
|
|
import type { Id } from "@/convex/_generated/dataModel"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip"
|
|
import { Usb, Shield, ShieldOff, ShieldAlert, Clock, CheckCircle2, XCircle, Loader2, History } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { formatDistanceToNow } from "date-fns"
|
|
import { ptBR } from "date-fns/locale"
|
|
|
|
type UsbPolicyValue = "ALLOW" | "BLOCK_ALL" | "READONLY"
|
|
|
|
interface UsbPolicyEvent {
|
|
id: string
|
|
oldPolicy?: string
|
|
newPolicy: string
|
|
status: string
|
|
error?: string
|
|
actorEmail?: string
|
|
actorName?: string
|
|
createdAt: number
|
|
appliedAt?: number
|
|
}
|
|
|
|
const POLICY_OPTIONS: Array<{ value: UsbPolicyValue; label: string; description: string; icon: typeof Shield }> = [
|
|
{
|
|
value: "ALLOW",
|
|
label: "Permitido",
|
|
description: "Acesso total a dispositivos USB de armazenamento",
|
|
icon: Shield,
|
|
},
|
|
{
|
|
value: "BLOCK_ALL",
|
|
label: "Bloqueado",
|
|
description: "Nenhum acesso a dispositivos USB de armazenamento",
|
|
icon: ShieldOff,
|
|
},
|
|
{
|
|
value: "READONLY",
|
|
label: "Somente leitura",
|
|
description: "Permite leitura, bloqueia escrita em dispositivos USB",
|
|
icon: ShieldAlert,
|
|
},
|
|
]
|
|
|
|
function getPolicyConfig(policy: string | undefined | null) {
|
|
return POLICY_OPTIONS.find((opt) => opt.value === policy) ?? POLICY_OPTIONS[0]
|
|
}
|
|
|
|
function getStatusBadge(status: string | undefined | null) {
|
|
switch (status) {
|
|
case "PENDING":
|
|
return (
|
|
<Badge variant="outline" className="gap-1 border-amber-200 bg-amber-50 text-amber-700">
|
|
<Clock className="size-3" />
|
|
Pendente
|
|
</Badge>
|
|
)
|
|
case "APPLIED":
|
|
return (
|
|
<Badge variant="outline" className="gap-1 border-emerald-200 bg-emerald-50 text-emerald-700">
|
|
<CheckCircle2 className="size-3" />
|
|
Aplicado
|
|
</Badge>
|
|
)
|
|
case "FAILED":
|
|
return (
|
|
<Badge variant="outline" className="gap-1 border-red-200 bg-red-50 text-red-700">
|
|
<XCircle className="size-3" />
|
|
Falhou
|
|
</Badge>
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
interface UsbPolicyControlProps {
|
|
machineId: string
|
|
machineName?: string
|
|
actorEmail?: string
|
|
actorName?: string
|
|
actorId?: string
|
|
disabled?: boolean
|
|
variant?: "card" | "inline"
|
|
}
|
|
|
|
export function UsbPolicyControl({
|
|
machineId,
|
|
machineName,
|
|
actorEmail,
|
|
actorName,
|
|
actorId,
|
|
disabled = false,
|
|
variant = "card",
|
|
}: UsbPolicyControlProps) {
|
|
const [selectedPolicy, setSelectedPolicy] = useState<UsbPolicyValue>("ALLOW")
|
|
const [isApplying, setIsApplying] = useState(false)
|
|
const [showHistory, setShowHistory] = useState(false)
|
|
|
|
const usbPolicy = useQuery(api.usbPolicy.getUsbPolicy, {
|
|
machineId: machineId as Id<"machines">,
|
|
})
|
|
|
|
const policyEvents = useQuery(
|
|
api.usbPolicy.listUsbPolicyEvents,
|
|
showHistory ? { machineId: machineId as Id<"machines">, limit: 10 } : "skip"
|
|
)
|
|
|
|
const setUsbPolicyMutation = useMutation(api.usbPolicy.setUsbPolicy)
|
|
|
|
useEffect(() => {
|
|
if (usbPolicy?.policy) {
|
|
setSelectedPolicy(usbPolicy.policy as UsbPolicyValue)
|
|
}
|
|
}, [usbPolicy?.policy])
|
|
|
|
const currentConfig = getPolicyConfig(usbPolicy?.policy)
|
|
const CurrentIcon = currentConfig.icon
|
|
|
|
const handleApplyPolicy = async () => {
|
|
if (selectedPolicy === usbPolicy?.policy) {
|
|
toast.info("A política selecionada já está aplicada.")
|
|
return
|
|
}
|
|
|
|
setIsApplying(true)
|
|
try {
|
|
await setUsbPolicyMutation({
|
|
machineId: machineId as Id<"machines">,
|
|
policy: selectedPolicy,
|
|
actorId: actorId ? (actorId as Id<"users">) : undefined,
|
|
actorEmail,
|
|
actorName,
|
|
})
|
|
toast.success("Política USB enviada para aplicação.")
|
|
} catch (error) {
|
|
console.error("[usb-policy] Falha ao aplicar política", error)
|
|
toast.error("Falha ao aplicar política USB. Tente novamente.")
|
|
} finally {
|
|
setIsApplying(false)
|
|
}
|
|
}
|
|
|
|
const formatEventDate = (timestamp: number) => {
|
|
return formatDistanceToNow(new Date(timestamp), {
|
|
addSuffix: true,
|
|
locale: ptBR,
|
|
})
|
|
}
|
|
|
|
const content = (
|
|
<div className="space-y-4">
|
|
{/* Status atual no topo */}
|
|
{usbPolicy?.status && (
|
|
<div className="flex items-center justify-between rounded-lg border bg-slate-50 p-3">
|
|
<span className="text-sm font-medium text-slate-600">Status da política</span>
|
|
{getStatusBadge(usbPolicy.status)}
|
|
</div>
|
|
)}
|
|
|
|
{usbPolicy?.error && (
|
|
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
|
<p className="text-sm font-medium text-red-700">Erro na aplicação</p>
|
|
<p className="text-xs text-red-600">{usbPolicy.error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Política atual */}
|
|
<div className="flex items-center gap-3 rounded-lg border bg-neutral-50 p-3">
|
|
<div className="flex size-10 items-center justify-center rounded-full bg-white shadow-sm">
|
|
<CurrentIcon className="size-5 text-neutral-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{currentConfig.label}</p>
|
|
<p className="text-xs text-muted-foreground">{currentConfig.description}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-end gap-2">
|
|
<div className="flex-1 space-y-1.5">
|
|
<label className="text-xs font-medium text-muted-foreground">Alterar política</label>
|
|
<Select
|
|
value={selectedPolicy}
|
|
onValueChange={(value) => setSelectedPolicy(value as UsbPolicyValue)}
|
|
disabled={disabled || isApplying}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Selecione uma política" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{POLICY_OPTIONS.map((option) => {
|
|
const Icon = option.icon
|
|
return (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
<div className="flex items-center gap-2">
|
|
<Icon className="size-4" />
|
|
<span>{option.label}</span>
|
|
</div>
|
|
</SelectItem>
|
|
)
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
onClick={handleApplyPolicy}
|
|
disabled={disabled || isApplying || selectedPolicy === usbPolicy?.policy}
|
|
size="default"
|
|
>
|
|
{isApplying ? (
|
|
<>
|
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
Aplicando...
|
|
</>
|
|
) : (
|
|
"Aplicar"
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{selectedPolicy === usbPolicy?.policy
|
|
? "A política já está aplicada"
|
|
: `Aplicar política "${getPolicyConfig(selectedPolicy).label}"`}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
|
|
<div className="border-t pt-3">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-start gap-2 text-muted-foreground"
|
|
onClick={() => setShowHistory(!showHistory)}
|
|
>
|
|
<History className="size-4" />
|
|
{showHistory ? "Ocultar histórico" : "Ver histórico de alterações"}
|
|
</Button>
|
|
|
|
{showHistory && policyEvents && (
|
|
<div className="mt-3 space-y-2">
|
|
{policyEvents.length === 0 ? (
|
|
<p className="text-center text-xs text-muted-foreground py-2">
|
|
Nenhuma alteração registrada
|
|
</p>
|
|
) : (
|
|
policyEvents.map((event: UsbPolicyEvent) => (
|
|
<div
|
|
key={event.id}
|
|
className="flex items-start justify-between rounded-md border bg-white p-2 text-xs"
|
|
>
|
|
<div className="space-y-0.5">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="font-medium">
|
|
{getPolicyConfig(event.oldPolicy).label}
|
|
</span>
|
|
<span className="text-muted-foreground">→</span>
|
|
<span className="font-medium">
|
|
{getPolicyConfig(event.newPolicy).label}
|
|
</span>
|
|
{getStatusBadge(event.status)}
|
|
</div>
|
|
<p className="text-muted-foreground">
|
|
{event.actorName ?? event.actorEmail ?? "Sistema"} · {formatEventDate(event.createdAt)}
|
|
</p>
|
|
{event.error && (
|
|
<p className="text-red-600">{event.error}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{usbPolicy?.reportedAt && (
|
|
<p className="text-xs text-muted-foreground">
|
|
Último relato do agente: {formatEventDate(usbPolicy.reportedAt)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
|
|
if (variant === "inline") {
|
|
return content
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Usb className="size-5 text-neutral-500" />
|
|
<CardTitle className="text-base">Controle USB</CardTitle>
|
|
</div>
|
|
</div>
|
|
<CardDescription>
|
|
Gerencie o acesso a dispositivos de armazenamento USB neste dispositivo.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{content}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|