"use client" import { MonitorIcon } from '../icons-v2-generated/devices/monitor-icon' import { Ellipsis01Icon } from '../icons-v2-generated/interface' import React from 'react' import { cn } from '../../utils/cn' import type { OSPlatformId } from '../../utils/os-platforms' import { OSTypeIcon } from '../features/os-type-badge' import { Tag, type TagProps } from './tag' export interface Device { id?: string machineId?: string name: string type?: 'desktop' | 'laptop' | 'mobile' | 'tablet' | 'server' operatingSystem?: OSPlatformId | 'macos' | 'ios' | 'android' // Support both OSPlatformId and legacy values organization?: string status?: 'active' | 'inactive' | 'offline' | 'warning' | 'error' lastSeen?: string | Date tags?: string[] // Additional device properties ipAddress?: string macAddress?: string version?: string location?: string } // Action button configuration export interface ActionButton { label: string onClick?: () => void variant?: 'default' | 'outline' | 'secondary' visible?: boolean } export interface DeviceCardProps extends React.HTMLAttributes { device: Device actions?: { moreButton?: { visible?: boolean onClick?: () => void } detailsButton?: { visible?: boolean component?: React.ReactNode } customActions?: ActionButton[] } statusTag?: TagProps onDeviceClick?: (device: Device) => void } export function DeviceCard({ device, actions = { moreButton: { visible: true } }, statusTag, onDeviceClick, className, ...props }: DeviceCardProps) { // Format date for last seen const formatLastSeen = (lastSeen?: string | Date) => { if (!lastSeen) return null const date = typeof lastSeen === 'string' ? new Date(lastSeen) : lastSeen // UTC getters so "last seen" is identical on server (UTC) and client // (local) — otherwise React #418 hydration mismatch. const year = date.getUTCFullYear() const month = String(date.getUTCMonth() + 1).padStart(2, '0') const day = String(date.getUTCDate()).padStart(2, '0') const hours = String(date.getUTCHours()).padStart(2, '0') const minutes = String(date.getUTCMinutes()).padStart(2, '0') return `${year}/${month}/${day}, ${hours}:${minutes}` } return (
onDeviceClick(device) : undefined} className={cn( "bg-ods-card rounded-[6px] border border-ods-border overflow-clip", "flex flex-col gap-4 p-4", onDeviceClick && "cursor-pointer", className )} {...props} > {/* Row 1: Device icon | OS icon + Name + Organization | More button | Details button | Custom actions */}
{/* Device type icon */}
{/* OS icon + Device name + Organization (stacked) */}
{device.operatingSystem && ( )} {device.name}
{device.organization && ( {device.organization} )}
{/* More button */} {actions.moreButton?.visible !== false && (
{ e.stopPropagation(); actions.moreButton?.onClick?.() }} >
)} {/* Details button */} {actions.detailsButton?.visible !== false && actions.detailsButton?.component && (
e.stopPropagation()}> {actions.detailsButton.component}
)} {/* Custom action buttons */} {actions.customActions?.map((action, index) => action.visible !== false && (
{ e.stopPropagation(); action.onClick?.() }} > {action.label}
) )}
{/* Row 2: Status badge | Last seen */} {(statusTag || device.lastSeen) &&
{statusTag && ( )} {device.lastSeen && ( Last Seen: {formatLastSeen(device.lastSeen)} )}
} {/* Tags section */} {device.tags && device.tags.length > 0 && (
{device.tags.map((tag, index) => ( ))}
)}
) }