Move USB policy control to modal dialog

- Add USB modal state and clickable InfoChip for USB policy chip
- Create Dialog with UsbPolicyControl component for USB management
- Add variant prop to UsbPolicyControl (card/inline) for flexible rendering
- Remove inline UsbPolicyControl from bottom of device page
- USB control now accessible by clicking USB chip in device summary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-05 09:36:50 -03:00
parent 6007cf6740
commit 06ebad930c
2 changed files with 183 additions and 140 deletions

View file

@ -3129,7 +3129,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
const hasRemoteAccess = remoteAccessEntries.length > 0 const hasRemoteAccess = remoteAccessEntries.length > 0
const summaryChips = useMemo(() => { const summaryChips = useMemo(() => {
const chips: Array<{ key: string; label: string; value: string; icon: ReactNode; tone?: "warning" | "muted" }> = [] const chips: Array<{ key: string; label: string; value: string; icon: ReactNode; tone?: "warning" | "muted"; onClick?: () => void }> = []
const osName = osNameDisplay || "Sistema desconhecido" const osName = osNameDisplay || "Sistema desconhecido"
const osVersionRaw = device?.osVersion ?? windowsVersionLabel ?? "" const osVersionRaw = device?.osVersion ?? windowsVersionLabel ?? ""
const osVersion = formatOsVersionDisplay(osNameDisplay, osVersionRaw) const osVersion = formatOsVersionDisplay(osNameDisplay, osVersionRaw)
@ -3259,6 +3259,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
const [togglingActive, setTogglingActive] = useState(false) const [togglingActive, setTogglingActive] = useState(false)
const [isResettingAgent, setIsResettingAgent] = useState(false) const [isResettingAgent, setIsResettingAgent] = useState(false)
const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false) const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false)
const [isUsbModalOpen, setIsUsbModalOpen] = useState(false)
const jsonText = useMemo(() => { const jsonText = useMemo(() => {
const payload = { const payload = {
id: device?.id, id: device?.id,
@ -3927,10 +3928,40 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
{/* ping integrado na badge de status */} {/* ping integrado na badge de status */}
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{summaryChips.map((chip) => ( {summaryChips.map((chip) => (
<InfoChip key={chip.key} label={chip.label} value={chip.value} icon={chip.icon} tone={chip.tone} /> <InfoChip
key={chip.key}
label={chip.label}
value={chip.value}
icon={chip.icon}
tone={chip.tone}
onClick={chip.key === "usb-policy" && canManageRemoteAccess ? () => setIsUsbModalOpen(true) : undefined}
/>
))} ))}
</div> </div>
{/* Modal de Controle USB */}
<Dialog open={isUsbModalOpen} onOpenChange={setIsUsbModalOpen}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Usb className="size-5" />
Controle de USB - {device.displayName ?? device.hostname}
</DialogTitle>
<DialogDescription>
Gerencie as politicas de acesso a dispositivos USB de armazenamento
</DialogDescription>
</DialogHeader>
<UsbPolicyControl
machineId={device.id}
machineName={device.displayName ?? device.hostname}
actorEmail={primaryLinkedUser?.email}
actorName={primaryLinkedUser?.name}
disabled={isDeactivated}
variant="inline"
/>
</DialogContent>
</Dialog>
<div className="rounded-2xl border border-slate-200 bg-white/80 px-4 py-4 shadow-sm"> <div className="rounded-2xl border border-slate-200 bg-white/80 px-4 py-4 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Controles do dispositivo</p> <p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Controles do dispositivo</p>
@ -5257,16 +5288,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null} ) : null}
</CardContent> </CardContent>
</Card> </Card>
{canManageRemoteAccess && device?.osName?.toLowerCase().includes("windows") ? (
<UsbPolicyControl
machineId={device.id}
machineName={device.displayName ?? device.hostname}
actorEmail={primaryLinkedUser?.email}
actorName={primaryLinkedUser?.name}
disabled={isDeactivated}
/>
) : null}
</div> </div>
{windowsCpuDetails.length > 0 ? ( {windowsCpuDetails.length > 0 ? (
@ -6196,7 +6217,7 @@ function DetailLine({ label, value, classNameValue, layout = "spread" }: DetailL
) )
} }
function InfoChip({ label, value, icon, tone = "default" }: { label: string; value: string; icon?: ReactNode; tone?: "default" | "warning" | "muted" }) { function InfoChip({ label, value, icon, tone = "default", onClick }: { label: string; value: string; icon?: ReactNode; tone?: "default" | "warning" | "muted"; onClick?: () => void }) {
const toneClasses = const toneClasses =
tone === "warning" tone === "warning"
? "border-amber-200 bg-amber-50 text-amber-700" ? "border-amber-200 bg-amber-50 text-amber-700"
@ -6204,8 +6225,16 @@ function InfoChip({ label, value, icon, tone = "default" }: { label: string; val
? "border-slate-200 bg-slate-50 text-neutral-600" ? "border-slate-200 bg-slate-50 text-neutral-600"
: "border-slate-200 bg-white text-neutral-800" : "border-slate-200 bg-white text-neutral-800"
const clickableClasses = onClick ? "cursor-pointer hover:ring-2 hover:ring-blue-200 transition-all" : ""
return ( return (
<div className={cn("flex items-center gap-3 rounded-xl border px-3 py-2 shadow-sm", toneClasses)}> <div
className={cn("flex items-center gap-3 rounded-xl border px-3 py-2 shadow-sm", toneClasses, clickableClasses)}
onClick={onClick}
role={onClick ? "button" : undefined}
tabIndex={onClick ? 0 : undefined}
onKeyDown={onClick ? (e) => { if (e.key === "Enter" || e.key === " ") onClick() } : undefined}
>
{icon ? <span className="text-neutral-500">{icon}</span> : null} {icon ? <span className="text-neutral-500">{icon}</span> : null}
<div className="min-w-0 leading-tight"> <div className="min-w-0 leading-tight">
<p className="text-xs uppercase text-neutral-500">{label}</p> <p className="text-xs uppercase text-neutral-500">{label}</p>

View file

@ -99,6 +99,7 @@ interface UsbPolicyControlProps {
actorName?: string actorName?: string
actorId?: string actorId?: string
disabled?: boolean disabled?: boolean
variant?: "card" | "inline"
} }
export function UsbPolicyControl({ export function UsbPolicyControl({
@ -108,6 +109,7 @@ export function UsbPolicyControl({
actorName, actorName,
actorId, actorId,
disabled = false, disabled = false,
variant = "card",
}: UsbPolicyControlProps) { }: UsbPolicyControlProps) {
const [selectedPolicy, setSelectedPolicy] = useState<UsbPolicyValue>("ALLOW") const [selectedPolicy, setSelectedPolicy] = useState<UsbPolicyValue>("ALLOW")
const [isApplying, setIsApplying] = useState(false) const [isApplying, setIsApplying] = useState(false)
@ -135,7 +137,7 @@ export function UsbPolicyControl({
const handleApplyPolicy = async () => { const handleApplyPolicy = async () => {
if (selectedPolicy === usbPolicy?.policy) { if (selectedPolicy === usbPolicy?.policy) {
toast.info("A política selecionada já está aplicada.") toast.info("A politica selecionada ja esta aplicada.")
return return
} }
@ -148,10 +150,10 @@ export function UsbPolicyControl({
actorEmail, actorEmail,
actorName, actorName,
}) })
toast.success("Política USB enviada para aplicação.") toast.success("Politica USB enviada para aplicacao.")
} catch (error) { } catch (error) {
console.error("[usb-policy] Falha ao aplicar política", error) console.error("[usb-policy] Falha ao aplicar politica", error)
toast.error("Falha ao aplicar política USB. Tente novamente.") toast.error("Falha ao aplicar politica USB. Tente novamente.")
} finally { } finally {
setIsApplying(false) setIsApplying(false)
} }
@ -164,22 +166,10 @@ export function UsbPolicyControl({
}) })
} }
return ( const content = (
<Card> <div className="space-y-4">
<CardHeader className="pb-3"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center gap-3 rounded-lg border bg-neutral-50 p-3 flex-1">
<div className="flex items-center gap-2">
<Usb className="size-5 text-neutral-500" />
<CardTitle className="text-base">Controle USB</CardTitle>
</div>
{usbPolicy?.status && getStatusBadge(usbPolicy.status)}
</div>
<CardDescription>
Gerencie o acesso a dispositivos de armazenamento USB neste dispositivo.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<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"> <div className="flex size-10 items-center justify-center rounded-full bg-white shadow-sm">
<CurrentIcon className="size-5 text-neutral-600" /> <CurrentIcon className="size-5 text-neutral-600" />
</div> </div>
@ -188,24 +178,26 @@ export function UsbPolicyControl({
<p className="text-xs text-muted-foreground">{currentConfig.description}</p> <p className="text-xs text-muted-foreground">{currentConfig.description}</p>
</div> </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 aplicação</p> <p className="text-sm font-medium text-red-700">Erro na aplicacao</p>
<p className="text-xs text-red-600">{usbPolicy.error}</p> <p className="text-xs text-red-600">{usbPolicy.error}</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 política</label> <label className="text-xs font-medium text-muted-foreground">Alterar politica</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 política" /> <SelectValue placeholder="Selecione uma politica" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{POLICY_OPTIONS.map((option) => { {POLICY_OPTIONS.map((option) => {
@ -242,8 +234,8 @@ export function UsbPolicyControl({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{selectedPolicy === usbPolicy?.policy {selectedPolicy === usbPolicy?.policy
? "A política já está aplicada" ? "A politica ja esta aplicada"
: `Aplicar política "${getPolicyConfig(selectedPolicy).label}"`} : `Aplicar politica "${getPolicyConfig(selectedPolicy).label}"`}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@ -257,14 +249,14 @@ export function UsbPolicyControl({
onClick={() => setShowHistory(!showHistory)} onClick={() => setShowHistory(!showHistory)}
> >
<History className="size-4" /> <History className="size-4" />
{showHistory ? "Ocultar histórico" : "Ver histórico de alterações"} {showHistory ? "Ocultar historico" : "Ver historico de alteracoes"}
</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 alteração registrada Nenhuma alteracao registrada
</p> </p>
) : ( ) : (
policyEvents.map((event: UsbPolicyEvent) => ( policyEvents.map((event: UsbPolicyEvent) => (
@ -277,14 +269,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">-&gt;</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>
@ -299,9 +291,31 @@ export function UsbPolicyControl({
{usbPolicy?.reportedAt && ( {usbPolicy?.reportedAt && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Último relato do agente: {formatEventDate(usbPolicy.reportedAt)} Ultimo relato do agente: {formatEventDate(usbPolicy.reportedAt)}
</p> </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> </CardContent>
</Card> </Card>
) )