Exibe status de reprovisionamento quando token é revogado
All checks were successful
All checks were successful
This commit is contained in:
parent
89f756e088
commit
b7e2c4cc98
2 changed files with 60 additions and 19 deletions
|
|
@ -66,6 +66,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { ChartContainer } from "@/components/ui/chart"
|
import { ChartContainer } from "@/components/ui/chart"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { DEVICE_INVENTORY_COLUMN_METADATA, type DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns"
|
import { DEVICE_INVENTORY_COLUMN_METADATA, type DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns"
|
||||||
|
|
@ -1050,6 +1051,7 @@ const statusClasses: Record<string, string> = {
|
||||||
online: "border-emerald-200 text-emerald-600",
|
online: "border-emerald-200 text-emerald-600",
|
||||||
offline: "border-rose-200 text-rose-600",
|
offline: "border-rose-200 text-rose-600",
|
||||||
stale: "border-amber-200 text-amber-600",
|
stale: "border-amber-200 text-amber-600",
|
||||||
|
reprovision: "border-orange-200 text-orange-700",
|
||||||
maintenance: "border-amber-300 text-amber-700",
|
maintenance: "border-amber-300 text-amber-700",
|
||||||
blocked: "border-orange-200 text-orange-600",
|
blocked: "border-orange-200 text-orange-600",
|
||||||
deactivated: "border-slate-200 bg-slate-50 text-slate-500",
|
deactivated: "border-slate-200 bg-slate-50 text-slate-500",
|
||||||
|
|
@ -1314,6 +1316,16 @@ function getStatusVariant(status?: string | null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAdminDeviceStatus(device: DevicesQueryItem): string {
|
||||||
|
return resolveDeviceStatus({
|
||||||
|
status: device.status,
|
||||||
|
lastHeartbeatAt: device.lastHeartbeatAt,
|
||||||
|
isActive: device.isActive,
|
||||||
|
managementMode: device.managementMode,
|
||||||
|
hasValidToken: Boolean(device.token),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function OsIcon({ osName }: { osName?: string | null }) {
|
function OsIcon({ osName }: { osName?: string | null }) {
|
||||||
const name = (osName ?? "").toLowerCase()
|
const name = (osName ?? "").toLowerCase()
|
||||||
if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return <Apple className="size-4 text-neutral-500" />
|
if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return <Apple className="size-4 text-neutral-500" />
|
||||||
|
|
@ -1698,7 +1710,7 @@ export function AdminDevicesOverview({
|
||||||
return devices.filter((m) => {
|
return devices.filter((m) => {
|
||||||
if (onlyAlerts && !(Array.isArray(m.postureAlerts) && m.postureAlerts.length > 0)) return false
|
if (onlyAlerts && !(Array.isArray(m.postureAlerts) && m.postureAlerts.length > 0)) return false
|
||||||
if (statusFilter !== "all") {
|
if (statusFilter !== "all") {
|
||||||
const s = resolveDeviceStatus(m).toLowerCase()
|
const s = resolveAdminDeviceStatus(m).toLowerCase()
|
||||||
if (s !== statusFilter) return false
|
if (s !== statusFilter) return false
|
||||||
}
|
}
|
||||||
if (deviceTypeFilter !== "all") {
|
if (deviceTypeFilter !== "all") {
|
||||||
|
|
@ -1971,6 +1983,7 @@ export function AdminDevicesOverview({
|
||||||
<SelectItem value="online">Online</SelectItem>
|
<SelectItem value="online">Online</SelectItem>
|
||||||
<SelectItem value="offline">Offline</SelectItem>
|
<SelectItem value="offline">Offline</SelectItem>
|
||||||
<SelectItem value="stale">Sem sinal</SelectItem>
|
<SelectItem value="stale">Sem sinal</SelectItem>
|
||||||
|
<SelectItem value="reprovision">Reprovisionar</SelectItem>
|
||||||
<SelectItem value="unknown">Desconhecido</SelectItem>
|
<SelectItem value="unknown">Desconhecido</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
@ -2106,7 +2119,7 @@ export function AdminDevicesOverview({
|
||||||
<div className="max-h-80 overflow-y-auto rounded-md border border-slate-200">
|
<div className="max-h-80 overflow-y-auto rounded-md border border-slate-200">
|
||||||
<ul className="divide-y divide-slate-100">
|
<ul className="divide-y divide-slate-100">
|
||||||
{filteredDevices.map((device) => {
|
{filteredDevices.map((device) => {
|
||||||
const statusKey = resolveDeviceStatus(device)
|
const statusKey = resolveAdminDeviceStatus(device)
|
||||||
const statusLabel = DEVICE_STATUS_LABELS[statusKey] ?? statusKey
|
const statusLabel = DEVICE_STATUS_LABELS[statusKey] ?? statusKey
|
||||||
const isChecked = exportSelection.includes(device.id)
|
const isChecked = exportSelection.includes(device.id)
|
||||||
const osParts = [device.osName ?? "", device.osVersion ?? ""].filter(Boolean)
|
const osParts = [device.osName ?? "", device.osVersion ?? ""].filter(Boolean)
|
||||||
|
|
@ -2773,7 +2786,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
const normalizedViewerRole = (viewerRole ?? "").toLowerCase()
|
const normalizedViewerRole = (viewerRole ?? "").toLowerCase()
|
||||||
const canManageRemoteAccess = normalizedViewerRole === "admin" || normalizedViewerRole === "agent"
|
const canManageRemoteAccess = normalizedViewerRole === "admin" || normalizedViewerRole === "agent"
|
||||||
const canManageFieldCatalog = normalizedViewerRole === "admin"
|
const canManageFieldCatalog = normalizedViewerRole === "admin"
|
||||||
const effectiveStatus = device ? resolveDeviceStatus(device) : "unknown"
|
const effectiveStatus = device ? resolveAdminDeviceStatus(device) : "unknown"
|
||||||
const [isActiveLocal, setIsActiveLocal] = useState<boolean>(device?.isActive ?? true)
|
const [isActiveLocal, setIsActiveLocal] = useState<boolean>(device?.isActive ?? true)
|
||||||
const isDeactivated = !isActiveLocal || effectiveStatus === "deactivated"
|
const isDeactivated = !isActiveLocal || effectiveStatus === "deactivated"
|
||||||
const isManualMobile =
|
const isManualMobile =
|
||||||
|
|
@ -3195,6 +3208,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
const lastHeartbeatDate = device?.lastHeartbeatAt ? new Date(device.lastHeartbeatAt) : null
|
const lastHeartbeatDate = device?.lastHeartbeatAt ? new Date(device.lastHeartbeatAt) : null
|
||||||
const tokenExpiry = device?.token?.expiresAt ? new Date(device.token.expiresAt) : null
|
const tokenExpiry = device?.token?.expiresAt ? new Date(device.token.expiresAt) : null
|
||||||
const tokenLastUsed = device?.token?.lastUsedAt ? new Date(device.token.lastUsedAt) : null
|
const tokenLastUsed = device?.token?.lastUsedAt ? new Date(device.token.lastUsedAt) : null
|
||||||
|
const hasActiveToken = Boolean(device?.token)
|
||||||
|
|
||||||
const copyEmail = async () => {
|
const copyEmail = async () => {
|
||||||
if (!device?.authEmail) return
|
if (!device?.authEmail) return
|
||||||
|
|
@ -4024,9 +4038,14 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
<CardAction>
|
<CardAction>
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2 text-xs sm:text-sm">
|
<div className="flex flex-wrap items-center justify-end gap-2 text-xs sm:text-sm">
|
||||||
{companyName ? (
|
{companyName ? (
|
||||||
<div className="max-w-[200px] truncate rounded-lg border border-slate-200 bg-white px-3 py-1 font-semibold text-neutral-600 shadow-sm" title={companyName}>
|
<Tooltip>
|
||||||
{companyName}
|
<TooltipTrigger asChild>
|
||||||
</div>
|
<div className="max-w-[200px] truncate rounded-lg border border-slate-200 bg-white px-3 py-1 font-semibold text-neutral-600 shadow-sm">
|
||||||
|
{companyName}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{companyName}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
{!isDeactivated ? <DeviceStatusBadge status={effectiveStatus} /> : null}
|
{!isDeactivated ? <DeviceStatusBadge status={effectiveStatus} /> : null}
|
||||||
{!isActiveLocal ? (
|
{!isActiveLocal ? (
|
||||||
|
|
@ -4700,18 +4719,20 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span>Token expira</span>
|
<span>Token expira</span>
|
||||||
<span className="text-right font-medium text-foreground">
|
<span className="text-right font-medium text-foreground">
|
||||||
{tokenExpiry ? formatRelativeTime(tokenExpiry) : "—"}
|
{hasActiveToken ? (tokenExpiry ? formatRelativeTime(tokenExpiry) : "-") : "Sem token ativo"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span>Token usado por último</span>
|
<span>Token usado por último</span>
|
||||||
<span className="text-right font-medium text-foreground">
|
<span className="text-right font-medium text-foreground">
|
||||||
{tokenLastUsed ? formatRelativeTime(tokenLastUsed) : "—"}
|
{hasActiveToken ? (tokenLastUsed ? formatRelativeTime(tokenLastUsed) : "-") : "Sem token ativo"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span>Uso do token</span>
|
<span>Uso do token</span>
|
||||||
<span className="text-right font-medium text-foreground">{device.token?.usageCount ?? 0} trocas</span>
|
<span className="text-right font-medium text-foreground">
|
||||||
|
{hasActiveToken ? `${device.token?.usageCount ?? 0} trocas` : "Sem token ativo"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -6322,7 +6343,7 @@ function DevicesGrid({ devices, companyNameBySlug }: { devices: DevicesQueryItem
|
||||||
|
|
||||||
function DeviceCard({ device, companyName }: { device: DevicesQueryItem; companyName?: string | null }) {
|
function DeviceCard({ device, companyName }: { device: DevicesQueryItem; companyName?: string | null }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const effectiveStatus = resolveDeviceStatus(device)
|
const effectiveStatus = resolveAdminDeviceStatus(device)
|
||||||
const statusIndicator = getDeviceStatusIndicator(effectiveStatus)
|
const statusIndicator = getDeviceStatusIndicator(effectiveStatus)
|
||||||
const isActive = device.isActive
|
const isActive = device.isActive
|
||||||
const lastHeartbeat = device.lastHeartbeatAt ? new Date(device.lastHeartbeatAt) : null
|
const lastHeartbeat = device.lastHeartbeatAt ? new Date(device.lastHeartbeatAt) : null
|
||||||
|
|
@ -6406,15 +6427,27 @@ function DeviceCard({ device, companyName }: { device: DevicesQueryItem; company
|
||||||
</div>
|
</div>
|
||||||
{collaborator?.email ? (
|
{collaborator?.email ? (
|
||||||
<div className="mt-3 flex flex-col gap-2 text-xs text-slate-600">
|
<div className="mt-3 flex flex-col gap-2 text-xs text-slate-600">
|
||||||
<div className="flex flex-col rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5">
|
<Tooltip>
|
||||||
<span className="text-[11px] uppercase text-slate-400">Usuário vinculado</span>
|
<TooltipTrigger asChild>
|
||||||
<span className="line-clamp-1 text-sm font-medium text-slate-800" title={collaborator.name ?? collaborator.email}>
|
<div className="flex flex-col rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5">
|
||||||
{collaborator.name ?? "Usuário"}
|
<span className="text-[11px] uppercase text-slate-400">Usuário vinculado</span>
|
||||||
</span>
|
<span className="line-clamp-1 text-sm font-medium text-slate-800">
|
||||||
<span className="text-[11px] text-slate-500 line-clamp-1" title={collaborator.email}>
|
{collaborator.name ?? "Usuário"}
|
||||||
{collaborator.email}
|
</span>
|
||||||
</span>
|
<span className="text-[11px] text-slate-500 line-clamp-1">
|
||||||
</div>
|
{collaborator.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<span className="font-semibold">
|
||||||
|
{collaborator.name ?? collaborator.email}
|
||||||
|
</span>
|
||||||
|
<span>{collaborator.email}</span>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="self-start text-[11px] font-semibold text-slate-500 underline underline-offset-4 hover:text-slate-800"
|
className="self-start text-[11px] font-semibold text-slate-500 underline underline-offset-4 hover:text-slate-800"
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export const DEVICE_STATUS_LABELS: Record<string, string> = {
|
||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
stale: "Sem sinal",
|
stale: "Sem sinal",
|
||||||
|
reprovision: "Reprovisionar",
|
||||||
maintenance: "Em manutenção",
|
maintenance: "Em manutenção",
|
||||||
blocked: "Bloqueado",
|
blocked: "Bloqueado",
|
||||||
deactivated: "Desativado",
|
deactivated: "Desativado",
|
||||||
|
|
@ -31,6 +32,8 @@ export type DeviceStatusSource = {
|
||||||
status?: string | null
|
status?: string | null
|
||||||
lastHeartbeatAt?: number | null
|
lastHeartbeatAt?: number | null
|
||||||
isActive?: boolean | null
|
isActive?: boolean | null
|
||||||
|
hasValidToken?: boolean | null
|
||||||
|
managementMode?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDeviceStatus(source?: DeviceStatusSource | null): string {
|
export function resolveDeviceStatus(source?: DeviceStatusSource | null): string {
|
||||||
|
|
@ -40,6 +43,10 @@ export function resolveDeviceStatus(source?: DeviceStatusSource | null): string
|
||||||
if (manualStatus === "maintenance" || manualStatus === "blocked") {
|
if (manualStatus === "maintenance" || manualStatus === "blocked") {
|
||||||
return manualStatus
|
return manualStatus
|
||||||
}
|
}
|
||||||
|
const managementMode = (source.managementMode ?? "").toLowerCase()
|
||||||
|
if (managementMode === "agent" && source.hasValidToken === false) {
|
||||||
|
return "reprovision"
|
||||||
|
}
|
||||||
const heartbeat = source.lastHeartbeatAt
|
const heartbeat = source.lastHeartbeatAt
|
||||||
if (typeof heartbeat === "number" && Number.isFinite(heartbeat) && heartbeat > 0) {
|
if (typeof heartbeat === "number" && Number.isFinite(heartbeat) && heartbeat > 0) {
|
||||||
const age = Date.now() - heartbeat
|
const age = Date.now() - heartbeat
|
||||||
|
|
@ -57,6 +64,7 @@ const STATUS_INDICATORS: Record<
|
||||||
online: { dotClass: "bg-emerald-500", ringClass: "bg-emerald-400/30", isPinging: true },
|
online: { dotClass: "bg-emerald-500", ringClass: "bg-emerald-400/30", isPinging: true },
|
||||||
offline: { dotClass: "bg-rose-500", ringClass: "bg-rose-400/30", isPinging: false },
|
offline: { dotClass: "bg-rose-500", ringClass: "bg-rose-400/30", isPinging: false },
|
||||||
stale: { dotClass: "bg-amber-500", ringClass: "bg-amber-400/30", isPinging: false },
|
stale: { dotClass: "bg-amber-500", ringClass: "bg-amber-400/30", isPinging: false },
|
||||||
|
reprovision: { dotClass: "bg-orange-500", ringClass: "bg-orange-400/30", isPinging: false },
|
||||||
maintenance: { dotClass: "bg-amber-500", ringClass: "bg-amber-400/30", isPinging: false },
|
maintenance: { dotClass: "bg-amber-500", ringClass: "bg-amber-400/30", isPinging: false },
|
||||||
blocked: { dotClass: "bg-orange-500", ringClass: "bg-orange-400/30", isPinging: false },
|
blocked: { dotClass: "bg-orange-500", ringClass: "bg-orange-400/30", isPinging: false },
|
||||||
deactivated: { dotClass: "bg-slate-500", ringClass: "bg-slate-400/40", isPinging: false },
|
deactivated: { dotClass: "bg-slate-500", ringClass: "bg-slate-400/40", isPinging: false },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue