Refactor USB control to modal with improved UX
- 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>
This commit is contained in:
parent
06ebad930c
commit
1ee5b34158
2 changed files with 57 additions and 34 deletions
|
|
@ -40,6 +40,7 @@ import {
|
||||||
Tablet,
|
Tablet,
|
||||||
Usb,
|
Usb,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
|
|
@ -53,7 +54,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -3185,7 +3186,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const isWindowsDevice = (device?.osName ?? "").toLowerCase().includes("windows")
|
const isWindowsDevice = (device?.osName ?? "").toLowerCase().includes("windows")
|
||||||
if (isWindowsDevice && device?.usbPolicy) {
|
if (isWindowsDevice) {
|
||||||
const policyLabels: Record<string, string> = {
|
const policyLabels: Record<string, string> = {
|
||||||
ALLOW: "Permitido",
|
ALLOW: "Permitido",
|
||||||
BLOCK_ALL: "Bloqueado",
|
BLOCK_ALL: "Bloqueado",
|
||||||
|
|
@ -3196,10 +3197,11 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
APPLIED: "Aplicado",
|
APPLIED: "Aplicado",
|
||||||
FAILED: "Falhou",
|
FAILED: "Falhou",
|
||||||
}
|
}
|
||||||
const policyLabel = policyLabels[device.usbPolicy] ?? device.usbPolicy
|
const currentPolicy = device?.usbPolicy ?? "ALLOW"
|
||||||
const statusLabel = device.usbPolicyStatus ? ` (${statusLabels[device.usbPolicyStatus] ?? device.usbPolicyStatus})` : ""
|
const policyLabel = policyLabels[currentPolicy] ?? currentPolicy
|
||||||
const isPending = device.usbPolicyStatus === "PENDING"
|
const statusLabel = device?.usbPolicyStatus ? ` (${statusLabels[device.usbPolicyStatus] ?? device.usbPolicyStatus})` : ""
|
||||||
const isFailed = device.usbPolicyStatus === "FAILED"
|
const isPending = device?.usbPolicyStatus === "PENDING"
|
||||||
|
const isFailed = device?.usbPolicyStatus === "FAILED"
|
||||||
chips.push({
|
chips.push({
|
||||||
key: "usb-policy",
|
key: "usb-policy",
|
||||||
label: "USB",
|
label: "USB",
|
||||||
|
|
@ -3934,7 +3936,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
value={chip.value}
|
value={chip.value}
|
||||||
icon={chip.icon}
|
icon={chip.icon}
|
||||||
tone={chip.tone}
|
tone={chip.tone}
|
||||||
onClick={chip.key === "usb-policy" && canManageRemoteAccess ? () => setIsUsbModalOpen(true) : undefined}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3942,13 +3943,17 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
{/* Modal de Controle USB */}
|
{/* Modal de Controle USB */}
|
||||||
<Dialog open={isUsbModalOpen} onOpenChange={setIsUsbModalOpen}>
|
<Dialog open={isUsbModalOpen} onOpenChange={setIsUsbModalOpen}>
|
||||||
<DialogContent className="max-w-xl">
|
<DialogContent className="max-w-xl">
|
||||||
<DialogHeader>
|
<DialogClose className="absolute right-4 top-4 p-1.5 rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
||||||
|
<X className="size-4" />
|
||||||
|
<span className="sr-only">Fechar</span>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogHeader className="pb-4">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Usb className="size-5" />
|
<Usb className="size-5" />
|
||||||
Controle de USB - {device.displayName ?? device.hostname}
|
Controle de USB - {device.displayName ?? device.hostname}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Gerencie as politicas de acesso a dispositivos USB de armazenamento
|
Gerencie as políticas de acesso a dispositivos USB de armazenamento
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<UsbPolicyControl
|
<UsbPolicyControl
|
||||||
|
|
@ -4003,6 +4008,18 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canManageRemoteAccess && (device?.osName ?? "").toLowerCase().includes("windows") && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 border-dashed"
|
||||||
|
onClick={() => setIsUsbModalOpen(true)}
|
||||||
|
disabled={isDeactivated}
|
||||||
|
>
|
||||||
|
<Usb className="size-4" />
|
||||||
|
Controle USB
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export function UsbPolicyControl({
|
||||||
|
|
||||||
const handleApplyPolicy = async () => {
|
const handleApplyPolicy = async () => {
|
||||||
if (selectedPolicy === usbPolicy?.policy) {
|
if (selectedPolicy === usbPolicy?.policy) {
|
||||||
toast.info("A politica selecionada ja esta aplicada.")
|
toast.info("A política selecionada já está aplicada.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,10 +150,10 @@ export function UsbPolicyControl({
|
||||||
actorEmail,
|
actorEmail,
|
||||||
actorName,
|
actorName,
|
||||||
})
|
})
|
||||||
toast.success("Politica USB enviada para aplicacao.")
|
toast.success("Política USB enviada para aplicação.")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[usb-policy] Falha ao aplicar politica", error)
|
console.error("[usb-policy] Falha ao aplicar política", error)
|
||||||
toast.error("Falha ao aplicar politica USB. Tente novamente.")
|
toast.error("Falha ao aplicar política USB. Tente novamente.")
|
||||||
} finally {
|
} finally {
|
||||||
setIsApplying(false)
|
setIsApplying(false)
|
||||||
}
|
}
|
||||||
|
|
@ -168,36 +168,42 @@ export function UsbPolicyControl({
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between gap-2">
|
{/* Status atual no topo */}
|
||||||
<div className="flex items-center gap-3 rounded-lg border bg-neutral-50 p-3 flex-1">
|
{usbPolicy?.status && (
|
||||||
<div className="flex size-10 items-center justify-center rounded-full bg-white shadow-sm">
|
<div className="flex items-center justify-between rounded-lg border bg-slate-50 p-3">
|
||||||
<CurrentIcon className="size-5 text-neutral-600" />
|
<span className="text-sm font-medium text-slate-600">Status da política</span>
|
||||||
</div>
|
{getStatusBadge(usbPolicy.status)}
|
||||||
<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>
|
||||||
{usbPolicy?.status && getStatusBadge(usbPolicy.status)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{usbPolicy?.error && (
|
{usbPolicy?.error && (
|
||||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||||
<p className="text-sm font-medium text-red-700">Erro na aplicacao</p>
|
<p className="text-sm font-medium text-red-700">Erro na aplicação</p>
|
||||||
<p className="text-xs text-red-600">{usbPolicy.error}</p>
|
<p className="text-xs text-red-600">{usbPolicy.error}</p>
|
||||||
</div>
|
</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 items-end gap-2">
|
||||||
<div className="flex-1 space-y-1.5">
|
<div className="flex-1 space-y-1.5">
|
||||||
<label className="text-xs font-medium text-muted-foreground">Alterar politica</label>
|
<label className="text-xs font-medium text-muted-foreground">Alterar política</label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedPolicy}
|
value={selectedPolicy}
|
||||||
onValueChange={(value) => setSelectedPolicy(value as UsbPolicyValue)}
|
onValueChange={(value) => setSelectedPolicy(value as UsbPolicyValue)}
|
||||||
disabled={disabled || isApplying}
|
disabled={disabled || isApplying}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Selecione uma politica" />
|
<SelectValue placeholder="Selecione uma política" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{POLICY_OPTIONS.map((option) => {
|
{POLICY_OPTIONS.map((option) => {
|
||||||
|
|
@ -234,8 +240,8 @@ export function UsbPolicyControl({
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{selectedPolicy === usbPolicy?.policy
|
{selectedPolicy === usbPolicy?.policy
|
||||||
? "A politica ja esta aplicada"
|
? "A política já está aplicada"
|
||||||
: `Aplicar politica "${getPolicyConfig(selectedPolicy).label}"`}
|
: `Aplicar política "${getPolicyConfig(selectedPolicy).label}"`}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
@ -249,14 +255,14 @@ export function UsbPolicyControl({
|
||||||
onClick={() => setShowHistory(!showHistory)}
|
onClick={() => setShowHistory(!showHistory)}
|
||||||
>
|
>
|
||||||
<History className="size-4" />
|
<History className="size-4" />
|
||||||
{showHistory ? "Ocultar historico" : "Ver historico de alteracoes"}
|
{showHistory ? "Ocultar histórico" : "Ver histórico de alterações"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{showHistory && policyEvents && (
|
{showHistory && policyEvents && (
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{policyEvents.length === 0 ? (
|
{policyEvents.length === 0 ? (
|
||||||
<p className="text-center text-xs text-muted-foreground py-2">
|
<p className="text-center text-xs text-muted-foreground py-2">
|
||||||
Nenhuma alteracao registrada
|
Nenhuma alteração registrada
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
policyEvents.map((event: UsbPolicyEvent) => (
|
policyEvents.map((event: UsbPolicyEvent) => (
|
||||||
|
|
@ -269,14 +275,14 @@ export function UsbPolicyControl({
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{getPolicyConfig(event.oldPolicy).label}
|
{getPolicyConfig(event.oldPolicy).label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground">-></span>
|
<span className="text-muted-foreground">→</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{getPolicyConfig(event.newPolicy).label}
|
{getPolicyConfig(event.newPolicy).label}
|
||||||
</span>
|
</span>
|
||||||
{getStatusBadge(event.status)}
|
{getStatusBadge(event.status)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{event.actorName ?? event.actorEmail ?? "Sistema"} - {formatEventDate(event.createdAt)}
|
{event.actorName ?? event.actorEmail ?? "Sistema"} · {formatEventDate(event.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
{event.error && (
|
{event.error && (
|
||||||
<p className="text-red-600">{event.error}</p>
|
<p className="text-red-600">{event.error}</p>
|
||||||
|
|
@ -291,7 +297,7 @@ export function UsbPolicyControl({
|
||||||
|
|
||||||
{usbPolicy?.reportedAt && (
|
{usbPolicy?.reportedAt && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Ultimo relato do agente: {formatEventDate(usbPolicy.reportedAt)}
|
Último relato do agente: {formatEventDate(usbPolicy.reportedAt)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue