import { ArrowPathIcon } from "@heroicons/react/20/solid"; import { useEffect, useRef, useState } from "react"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { LogLevelTooltipInfo } from "~/components/LogLevelTooltipInfo"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server"; import { cn } from "~/utils/cn"; import { highlightSearchText } from "~/utils/logUtils"; import { v3RunSpanPath } from "~/utils/pathBuilder"; import { DateTimeAccurate } from "../primitives/DateTime"; import { Paragraph } from "../primitives/Paragraph"; import { Spinner } from "../primitives/Spinner"; import { Table, TableBlankRow, TableBody, TableCell, TableCellMenu, TableHeader, TableHeaderCell, TableRow, type TableVariant, } from "../primitives/Table"; import { LogLevel } from "./LogLevel"; type LogsTableProps = { logs: LogEntry[]; searchTerm?: string; isLoading?: boolean; isLoadingMore?: boolean; hasMore?: boolean; onLoadMore?: () => void; onCheckForMore?: () => void; variant?: TableVariant; selectedLogId?: string; onLogSelect?: (logId: string) => void; }; // Inner shadow for level highlighting (better scroll performance than border-l) function getLevelBoxShadow(level: LogEntry["level"]): string { switch (level) { case "ERROR": return "inset 2px 0 0 0 rgb(239, 68, 68)"; case "WARN": return "inset 2px 0 0 0 rgb(234, 179, 8)"; case "INFO": return "inset 2px 0 0 0 rgb(59, 130, 246)"; case "TRACE": return "inset 2px 0 0 0 rgb(168, 85, 247)"; case "DEBUG": default: return "none"; } } export function LogsTable({ logs, searchTerm, isLoading = false, isLoadingMore = false, hasMore = false, onLoadMore, onCheckForMore, selectedLogId, onLogSelect, }: LogsTableProps) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const loadMoreRef = useRef(null); const [showLoadMoreSpinner, setShowLoadMoreSpinner] = useState(false); // Show load more spinner only after 0.2 seconds of loading time useEffect(() => { if (!isLoadingMore) { setShowLoadMoreSpinner(false); return; } const timer = setTimeout(() => { setShowLoadMoreSpinner(true); }, 200); return () => clearTimeout(timer); }, [isLoadingMore]); // Intersection observer for infinite scroll useEffect(() => { if (!hasMore || isLoadingMore || !onLoadMore) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { onLoadMore(); } }, { threshold: 0.1 } ); const currentRef = loadMoreRef.current; if (currentRef) { observer.observe(currentRef); } return () => { if (currentRef) { observer.unobserve(currentRef); } }; }, [hasMore, isLoadingMore, onLoadMore]); return (
Time Run Task } disableTooltipHoverableContent > Level Message {logs.length === 0 ? ( window.location.reload()} /> ) : ( logs.map((log) => { const isSelected = selectedLogId === log.id; const runPath = v3RunSpanPath( organization, project, environment, { friendlyId: log.runId }, { spanId: log.spanId } ); const handleRowClick = () => onLogSelect?.(log.id); return ( {log.runId} {log.taskIdentifier} {highlightSearchText(log.message, searchTerm)} View run } /> ); }) )}
{/* Infinite scroll trigger */} {hasMore && logs.length > 0 && (
Loading more…
)} {/* Show all logs message with check for more button */} {!hasMore && logs.length > 0 && (
Showing all {logs.length} logs
)}
); } function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?: () => void }) { if (isLoading) return ; const handleRefresh = onRefresh ?? (() => window.location.reload()); return (
No logs match your filters. Try refreshing or modifying your filters.
); }