{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "interactive-logs-table-shadcnui", "type": "registry:block", "title": "Interactive Logs Table", "description": "Observability logs panel with animated filters, search, and expandable rows", "registryDependencies": [ "button" ], "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-shadcn/src/components/sections/interactive-logs-table.tsx", "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { Check, ChevronDown, Filter, Search } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\n\ntype LogLevel = \"info\" | \"warning\" | \"error\";\n\ninterface Log {\n id: string;\n timestamp: string;\n level: LogLevel;\n service: string;\n message: string;\n duration: string;\n status: string;\n tags: string[];\n}\n\ntype Filters = {\n level: string[];\n service: string[];\n status: string[];\n};\n\nconst SAMPLE_LOGS: Log[] = [\n {\n id: \"1\",\n timestamp: \"2024-11-08T14:32:45Z\",\n level: \"info\",\n service: \"api-gateway\",\n message: \"Request processed successfully\",\n duration: \"245ms\",\n status: \"200\",\n tags: [\"api\", \"success\"],\n },\n {\n id: \"2\",\n timestamp: \"2024-11-08T14:32:42Z\",\n level: \"warning\",\n service: \"cache-service\",\n message: \"Cache miss ratio exceeds threshold\",\n duration: \"1.2s\",\n status: \"warning\",\n tags: [\"cache\", \"performance\"],\n },\n {\n id: \"3\",\n timestamp: \"2024-11-08T14:32:40Z\",\n level: \"error\",\n service: \"database\",\n message: \"Connection timeout to replica\",\n duration: \"5.1s\",\n status: \"503\",\n tags: [\"db\", \"error\"],\n },\n {\n id: \"4\",\n timestamp: \"2024-11-08T14:32:38Z\",\n level: \"info\",\n service: \"auth-service\",\n message: \"User session created\",\n duration: \"156ms\",\n status: \"201\",\n tags: [\"auth\", \"session\"],\n },\n {\n id: \"5\",\n timestamp: \"2024-11-08T14:32:35Z\",\n level: \"info\",\n service: \"api-gateway\",\n message: \"Webhook delivered\",\n duration: \"432ms\",\n status: \"200\",\n tags: [\"webhook\", \"integration\"],\n },\n {\n id: \"6\",\n timestamp: \"2024-11-08T14:32:32Z\",\n level: \"error\",\n service: \"payment-service\",\n message: \"Payment gateway unavailable\",\n duration: \"2.3s\",\n status: \"502\",\n tags: [\"payment\", \"error\"],\n },\n {\n id: \"7\",\n timestamp: \"2024-11-08T14:32:30Z\",\n level: \"info\",\n service: \"search-service\",\n message: \"Index updated\",\n duration: \"876ms\",\n status: \"200\",\n tags: [\"search\", \"index\"],\n },\n {\n id: \"8\",\n timestamp: \"2024-11-08T14:32:28Z\",\n level: \"warning\",\n service: \"api-gateway\",\n message: \"Rate limit approaching\",\n duration: \"145ms\",\n status: \"429\",\n tags: [\"rate-limit\", \"warning\"],\n },\n];\n\nconst levelStyles: Record = {\n info: \"bg-blue-500/10 text-blue-600 dark:text-blue-400\",\n warning: \"bg-yellow-500/10 text-yellow-600 dark:text-yellow-400\",\n error: \"bg-red-500/10 text-red-600 dark:text-red-400\",\n};\n\nconst statusStyles: Record = {\n \"200\": \"text-green-600 dark:text-green-400\",\n \"201\": \"text-green-600 dark:text-green-400\",\n \"429\": \"text-yellow-600 dark:text-yellow-400\",\n \"502\": \"text-red-600 dark:text-red-400\",\n \"503\": \"text-red-600 dark:text-red-400\",\n warning: \"text-yellow-600 dark:text-yellow-400\",\n};\n\nfunction LogRow({\n log,\n expanded,\n onToggle,\n}: {\n log: Log;\n expanded: boolean;\n onToggle: () => void;\n}) {\n const formattedTime = new Date(log.timestamp).toLocaleTimeString(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n\n return (\n <>\n \n
\n \n \n \n\n \n {log.level}\n \n\n \n\n \n {log.service}\n \n\n

\n {log.message}\n

\n\n \n {log.status}\n \n\n \n {log.duration}\n \n
\n \n\n \n {expanded && (\n \n
\n
\n

\n Message\n

\n

\n {log.message}\n

\n
\n\n
\n
\n

\n Duration\n

\n

{log.duration}

\n
\n
\n

\n Timestamp\n

\n

\n {log.timestamp}\n

\n
\n
\n\n
\n

\n Tags\n

\n
\n {log.tags.map((tag) => (\n \n {tag}\n \n ))}\n
\n
\n
\n \n )}\n
\n \n );\n}\n\nfunction FilterPanel({\n filters,\n onChange,\n logs,\n}: {\n filters: Filters;\n onChange: (filters: Filters) => void;\n logs: Log[];\n}) {\n const levels = Array.from(new Set(logs.map((log) => log.level)));\n const services = Array.from(new Set(logs.map((log) => log.service)));\n const statuses = Array.from(new Set(logs.map((log) => log.status)));\n\n const toggleFilter = (category: keyof Filters, value: string) => {\n const current = filters[category];\n const updated = current.includes(value)\n ? current.filter((entry) => entry !== value)\n : [...current, value];\n\n onChange({\n ...filters,\n [category]: updated,\n });\n };\n\n const clearAll = () => {\n onChange({\n level: [],\n service: [],\n status: [],\n });\n };\n\n const hasActiveFilters = Object.values(filters).some(\n (group) => group.length > 0\n );\n\n return (\n \n
\n

Filters

\n {hasActiveFilters && (\n \n Clear\n \n )}\n
\n\n
\n

\n Level\n

\n
\n {levels.map((level) => {\n const selected = filters.level.includes(level);\n\n return (\n toggleFilter(\"level\", level)}\n aria-pressed={selected}\n className={`flex w-full items-center justify-between gap-2 border rounded-md px-3 py-2 text-sm transition-colors ${\n selected\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/40 hover:bg-muted/40\"\n }`}\n >\n {level}\n {selected && }\n \n );\n })}\n
\n
\n\n
\n

\n Service\n

\n
\n {services.map((service) => {\n const selected = filters.service.includes(service);\n\n return (\n toggleFilter(\"service\", service)}\n aria-pressed={selected}\n className={`flex w-full items-center justify-between gap-2 border rounded-md px-3 py-2 text-sm transition-colors ${\n selected\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/40 hover:bg-muted/40\"\n }`}\n >\n {service}\n {selected && }\n \n );\n })}\n
\n
\n\n
\n

\n Status\n

\n
\n {statuses.map((status) => {\n const selected = filters.status.includes(status);\n\n return (\n toggleFilter(\"status\", status)}\n aria-pressed={selected}\n className={`flex w-full items-center justify-between gap-2 border rounded-md px-3 py-2 text-sm transition-colors ${\n selected\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/40 hover:bg-muted/40\"\n }`}\n >\n {status}\n {selected && }\n \n );\n })}\n
\n
\n \n );\n}\n\nexport function InteractiveLogsTable() {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [expandedId, setExpandedId] = useState(null);\n const [showFilters, setShowFilters] = useState(false);\n const [filters, setFilters] = useState({\n level: [],\n service: [],\n status: [],\n });\n\n const filteredLogs = useMemo(() => {\n return SAMPLE_LOGS.filter((log) => {\n const lowerQuery = searchQuery.toLowerCase();\n\n const matchSearch =\n log.message.toLowerCase().includes(lowerQuery) ||\n log.service.toLowerCase().includes(lowerQuery);\n\n const matchLevel =\n filters.level.length === 0 || filters.level.includes(log.level);\n const matchService =\n filters.service.length === 0 || filters.service.includes(log.service);\n const matchStatus =\n filters.status.length === 0 || filters.status.includes(log.status);\n\n return matchSearch && matchLevel && matchService && matchStatus;\n });\n }, [filters, searchQuery]);\n\n const activeFilters =\n filters.level.length + filters.service.length + filters.status.length;\n\n return (\n
\n
\n
\n
\n
\n

Logs

\n

\n {filteredLogs.length} of {SAMPLE_LOGS.length} logs\n

\n
\n\n
\n
\n \n setSearchQuery(event.target.value)}\n className=\"h-9 pl-9 text-sm\"\n />\n
\n setShowFilters((current) => !current)}\n className=\"relative\"\n >\n \n {activeFilters > 0 && (\n \n {activeFilters}\n \n )}\n \n
\n
\n
\n\n
\n \n {showFilters && (\n \n \n \n )}\n \n\n
\n
\n \n {filteredLogs.length > 0 ? (\n filteredLogs.map((log, index) => (\n \n \n setExpandedId((current) =>\n current === log.id ? null : log.id\n )\n }\n />\n \n ))\n ) : (\n \n

\n No logs match your filters.\n

\n \n )}\n
\n
\n
\n
\n
\n
\n );\n}\n", "type": "registry:block", "target": "components/uitripled/interactive-logs-table-shadcnui.tsx" } ] }