diff --git a/convex/usbPolicy.ts b/convex/usbPolicy.ts index e14d9b0..02e3781 100644 --- a/convex/usbPolicy.ts +++ b/convex/usbPolicy.ts @@ -117,7 +117,9 @@ export const reportUsbPolicyStatus = mutation({ .order("desc") .first() - if (latestEvent && latestEvent.status === "PENDING") { + // Atualiza o evento se ainda nao foi finalizado (PENDING ou APPLYING) + // Isso permite a transicao: PENDING -> APPLYING -> APPLIED/FAILED + if (latestEvent && (latestEvent.status === "PENDING" || latestEvent.status === "APPLYING")) { await ctx.db.patch(latestEvent._id, { status: args.status, error: errorValue, @@ -185,27 +187,57 @@ export const listUsbPolicyEvents = query({ args: { machineId: v.id("machines"), limit: v.optional(v.number()), + cursor: v.optional(v.number()), + status: v.optional(v.string()), + dateFrom: v.optional(v.number()), + dateTo: v.optional(v.number()), }, handler: async (ctx, args) => { - const limit = args.limit ?? 50 + const limit = args.limit ?? 10 - const events = await ctx.db + let events = await ctx.db .query("usbPolicyEvents") .withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId)) .order("desc") - .take(limit) + .collect() - return events.map((event) => ({ - id: event._id, - oldPolicy: event.oldPolicy, - newPolicy: event.newPolicy, - status: event.status, - error: event.error, - actorEmail: event.actorEmail, - actorName: event.actorName, - createdAt: event.createdAt, - appliedAt: event.appliedAt, - })) + // Aplica filtro de cursor (paginacao) + if (args.cursor !== undefined) { + events = events.filter((e) => e.createdAt < args.cursor!) + } + + // Aplica filtro de status + if (args.status) { + events = events.filter((e) => e.status === args.status) + } + + // Aplica filtro de data + if (args.dateFrom !== undefined) { + events = events.filter((e) => e.createdAt >= args.dateFrom!) + } + if (args.dateTo !== undefined) { + events = events.filter((e) => e.createdAt <= args.dateTo!) + } + + const hasMore = events.length > limit + const results = events.slice(0, limit) + const nextCursor = results.length > 0 ? results[results.length - 1].createdAt : undefined + + return { + events: results.map((event) => ({ + id: event._id, + oldPolicy: event.oldPolicy, + newPolicy: event.newPolicy, + status: event.status, + error: event.error, + actorEmail: event.actorEmail, + actorName: event.actorName, + createdAt: event.createdAt, + appliedAt: event.appliedAt, + })), + hasMore, + nextCursor, + } }, }) diff --git a/src/components/admin/devices/usb-policy-control.tsx b/src/components/admin/devices/usb-policy-control.tsx index 9ab30ac..e758ed7 100644 --- a/src/components/admin/devices/usb-policy-control.tsx +++ b/src/components/admin/devices/usb-policy-control.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, useMemo } from "react" import { useMutation, useQuery } from "convex/react" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" @@ -20,10 +20,11 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" -import { Usb, Shield, ShieldOff, ShieldAlert, Clock, CheckCircle2, XCircle, Loader2, History } from "lucide-react" +import { Usb, Shield, ShieldOff, ShieldAlert, Clock, CheckCircle2, XCircle, Loader2, History, Filter, ChevronDown, RotateCcw } from "lucide-react" import { toast } from "sonner" -import { formatDistanceToNow } from "date-fns" +import { formatDistanceToNow, startOfDay, endOfDay, parseISO } from "date-fns" import { ptBR } from "date-fns/locale" +import { DateRangeButton } from "@/components/date-range-button" type UsbPolicyValue = "ALLOW" | "BLOCK_ALL" | "READONLY" @@ -139,6 +140,14 @@ interface UsbPolicyControlProps { variant?: "card" | "inline" } +const STATUS_FILTER_OPTIONS = [ + { value: "all", label: "Todos os status" }, + { value: "PENDING", label: "Pendente" }, + { value: "APPLYING", label: "Aplicando" }, + { value: "APPLIED", label: "Aplicado" }, + { value: "FAILED", label: "Falhou" }, +] + export function UsbPolicyControl({ machineId, machineName, @@ -152,15 +161,77 @@ export function UsbPolicyControl({ const [isApplying, setIsApplying] = useState(false) const [showHistory, setShowHistory] = useState(false) + // Filtros do historico + const [statusFilter, setStatusFilter] = useState("all") + const [dateFrom, setDateFrom] = useState(null) + const [dateTo, setDateTo] = useState(null) + const [allEvents, setAllEvents] = useState([]) + const [cursor, setCursor] = useState(undefined) + const usbPolicy = useQuery(api.usbPolicy.getUsbPolicy, { machineId: machineId as Id<"machines">, }) - const policyEvents = useQuery( + // Converte datas para timestamp + const dateFromTs = useMemo(() => { + if (!dateFrom) return undefined + return startOfDay(parseISO(dateFrom)).getTime() + }, [dateFrom]) + + const dateToTs = useMemo(() => { + if (!dateTo) return undefined + return endOfDay(parseISO(dateTo)).getTime() + }, [dateTo]) + + const policyEventsResult = useQuery( api.usbPolicy.listUsbPolicyEvents, - showHistory ? { machineId: machineId as Id<"machines">, limit: 10 } : "skip" + showHistory + ? { + machineId: machineId as Id<"machines">, + limit: 10, + cursor, + status: statusFilter !== "all" ? statusFilter : undefined, + dateFrom: dateFromTs, + dateTo: dateToTs, + } + : "skip" ) + // Acumula eventos quando carrega mais + useEffect(() => { + if (policyEventsResult?.events) { + if (cursor === undefined) { + // Reset quando filtros mudam + setAllEvents(policyEventsResult.events) + } else { + // Acumula quando carrega mais + setAllEvents((prev) => [...prev, ...policyEventsResult.events]) + } + } + }, [policyEventsResult?.events, cursor]) + + // Reset cursor quando filtros mudam + useEffect(() => { + setCursor(undefined) + setAllEvents([]) + }, [statusFilter, dateFrom, dateTo]) + + const handleLoadMore = () => { + if (policyEventsResult?.nextCursor) { + setCursor(policyEventsResult.nextCursor) + } + } + + const handleResetFilters = () => { + setStatusFilter("all") + setDateFrom(null) + setDateTo(null) + setCursor(undefined) + setAllEvents([]) + } + + const hasActiveFilters = statusFilter !== "all" || dateFrom !== null || dateTo !== null + const setUsbPolicyMutation = useMutation(api.usbPolicy.setUsbPolicy) useEffect(() => { @@ -311,38 +382,99 @@ export function UsbPolicyControl({ {showHistory ? "Ocultar histórico" : "Ver histórico de alterações"} - {showHistory && policyEvents && ( -
- {policyEvents.length === 0 ? ( -

- Nenhuma alteração registrada + {showHistory && ( +

+ {/* Filtros */} +
+
+ + Filtros: +
+ + { + setDateFrom(from) + setDateTo(to) + }} + className="h-7 text-xs" + clearLabel="Limpar período" + /> + {hasActiveFilters && ( + + )} +
+ + {/* Lista de eventos */} + {allEvents.length === 0 ? ( +

+ {hasActiveFilters + ? "Nenhuma alteração encontrada com os filtros selecionados" + : "Nenhuma alteração registrada"}

) : ( - policyEvents.map((event: UsbPolicyEvent) => ( -
-
-
- - {getPolicyConfig(event.oldPolicy).label} - - - - {getPolicyConfig(event.newPolicy).label} - - {getStatusBadge(event.status)} + <> +
+ {allEvents.map((event: UsbPolicyEvent) => ( +
+
+
+ + {getPolicyConfig(event.oldPolicy).label} + + + + {getPolicyConfig(event.newPolicy).label} + + {getStatusBadge(event.status)} +
+

+ {event.actorName ?? event.actorEmail ?? "Sistema"} · {formatEventDate(event.createdAt)} +

+ {event.error && ( +

{event.error}

+ )} +
-

- {event.actorName ?? event.actorEmail ?? "Sistema"} · {formatEventDate(event.createdAt)} -

- {event.error && ( -

{event.error}

- )} -
+ ))}
- )) + + {/* Paginacao */} + {policyEventsResult?.hasMore && ( + + )} + )}
)}