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:
parent
6007cf6740
commit
06ebad930c
2 changed files with 183 additions and 140 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,120 +178,144 @@ 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 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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="border-t pt-3">
|
<div className="flex items-end gap-2">
|
||||||
<Button
|
<div className="flex-1 space-y-1.5">
|
||||||
variant="ghost"
|
<label className="text-xs font-medium text-muted-foreground">Alterar politica</label>
|
||||||
size="sm"
|
<Select
|
||||||
className="w-full justify-start gap-2 text-muted-foreground"
|
value={selectedPolicy}
|
||||||
onClick={() => setShowHistory(!showHistory)}
|
onValueChange={(value) => setSelectedPolicy(value as UsbPolicyValue)}
|
||||||
|
disabled={disabled || isApplying}
|
||||||
>
|
>
|
||||||
<History className="size-4" />
|
<SelectTrigger>
|
||||||
{showHistory ? "Ocultar histórico" : "Ver histórico de alterações"}
|
<SelectValue placeholder="Selecione uma politica" />
|
||||||
</Button>
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
{showHistory && policyEvents && (
|
{POLICY_OPTIONS.map((option) => {
|
||||||
<div className="mt-3 space-y-2">
|
const Icon = option.icon
|
||||||
{policyEvents.length === 0 ? (
|
return (
|
||||||
<p className="text-center text-xs text-muted-foreground py-2">
|
<SelectItem key={option.value} value={option.value}>
|
||||||
Nenhuma alteração registrada
|
<div className="flex items-center gap-2">
|
||||||
</p>
|
<Icon className="size-4" />
|
||||||
) : (
|
<span>{option.label}</span>
|
||||||
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>
|
</SelectItem>
|
||||||
))
|
)
|
||||||
)}
|
})}
|
||||||
</div>
|
</SelectContent>
|
||||||
)}
|
</Select>
|
||||||
</div>
|
</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 politica ja esta aplicada"
|
||||||
|
: `Aplicar politica "${getPolicyConfig(selectedPolicy).label}"`}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
{usbPolicy?.reportedAt && (
|
<div className="border-t pt-3">
|
||||||
<p className="text-xs text-muted-foreground">
|
<Button
|
||||||
Último relato do agente: {formatEventDate(usbPolicy.reportedAt)}
|
variant="ghost"
|
||||||
</p>
|
size="sm"
|
||||||
|
className="w-full justify-start gap-2 text-muted-foreground"
|
||||||
|
onClick={() => setShowHistory(!showHistory)}
|
||||||
|
>
|
||||||
|
<History className="size-4" />
|
||||||
|
{showHistory ? "Ocultar historico" : "Ver historico de alteracoes"}
|
||||||
|
</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 alteracao 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">
|
||||||
|
Ultimo 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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue