import { useMemo, useState } from 'react'; import { createColumnHelper, flexRender, getCoreRowModel, getSortedRowModel, SortingFn, SortingState, useReactTable, } from '@tanstack/react-table'; import type { ProcessedRequest } from '../state/model'; import type { NetworkEventSource, RequestId, RequestOverride, } from '../../shared/client'; import { useNetworkActivityActions, useOverrides, useSelectedRequestId, useClientUISettings, } from '../state/hooks'; import { getStatusColor } from '../utils/getStatusColor'; import { isNumber } from '../../utils/typeChecks'; type NetworkRequest = { id: RequestId; name: string; status: string | number; statusCode?: number; method: ProcessedRequest['method']; domain: string; path: string; contentType?: string; size: string; sizeBytes: number | null; time: string; durationMs: number; type: ProcessedRequest['type']; source?: NetworkEventSource; startTime: string; hasOverride: boolean; }; const getSourceLabel = (source?: NetworkEventSource) => { if (source === 'nitro') { return 'Nitro'; } if (source === 'builtin') { return 'Built-in'; } return null; }; const formatSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'kB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; const formatDuration = (duration: number): string => { if (duration < 1000) return `${Math.round(duration)} ms`; return `${(duration / 1000).toFixed(1)} s`; }; const formatStartTime = (startTime: number): string => { const date = new Date(startTime); const timeString = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }); const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); return `${timeString}.${milliseconds}`; }; const extractDomainAndPath = ( url: string, ): { domain: string; path: string } => { try { const { hostname, pathname, search, hash, port } = new URL(url); return { domain: `${hostname}${port ? `:${port}` : ''}`, path: `${pathname}${search}${hash}`, }; } catch { return { domain: 'unknown', path: url }; } }; const generateName = (url: string, showEntirePathName = false): string => { try { const urlObj = new URL(url); const pathname = urlObj.pathname; const filename = showEntirePathName ? undefined : pathname.split('/').pop(); return filename || pathname || urlObj.hostname; } catch { return url; } }; const sortSize: SortingFn = (rowA, rowB, columnId) => { const a = rowA.getValue(columnId) as string; const b = rowB.getValue(columnId) as string; const getNumericValue = (str: string) => { const match = str.match(/^([\d.]+)\s*([KMGT]?B)$/); if (!match) return 0; const value = parseFloat(match[1]); const unit = match[2]; const multipliers: Record = { B: 1, kB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024, }; return value * (multipliers[unit] || 1); }; return getNumericValue(a) - getNumericValue(b); }; const sortTime: SortingFn = (rowA, rowB, columnId) => { const a = rowA.getValue(columnId) as string; const b = rowB.getValue(columnId) as string; const getNumericValue = (str: string) => { const match = str.match(/^([\d.]+)\s*(ms|s)$/); if (!match) return 0; const value = parseFloat(match[1]); const unit = match[2]; return unit === 's' ? value * 1000 : value; }; return getNumericValue(a) - getNumericValue(b); }; const processNetworkRequests = ( processedRequests: ProcessedRequest[], overrides: Map, showEntirePathAsName = false, ): NetworkRequest[] => { return processedRequests.map((request): NetworkRequest => { const { domain, path } = extractDomainAndPath(request.name); const duration = request.duration || 0; const hasOverride = overrides.has(request.name); let statusDisplay: string | number = request.httpStatus || request.status; if (request.status === 'loading' && request.progress?.lengthComputable) { const percentage = Math.round( (request.progress.loaded / request.progress.total) * 100, ); statusDisplay = `${percentage}%`; } return { id: request.id, name: generateName(request.name, showEntirePathAsName), status: statusDisplay, statusCode: request.httpStatus || undefined, method: request.method, domain, path, contentType: request.contentType, size: isNumber(request.size) ? formatSize(request.size) : '—', sizeBytes: isNumber(request.size) ? request.size : null, time: formatDuration(duration), durationMs: duration, type: request.type, source: request.source, startTime: formatStartTime(request.timestamp), hasOverride, }; }); }; const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor('startTime', { header: 'Start Time', cell: ({ getValue }) =>
{getValue()}
, size: 120, sortingFn: 'basic', }), columnHelper.accessor('name', { header: 'Name', cell: ({ row, getValue }) => (
{getValue()} {row.original.hasOverride && ( )} {getSourceLabel(row.original.source) && ( {getSourceLabel(row.original.source)} )}
), sortingFn: 'alphanumeric', }), columnHelper.accessor('status', { header: 'Status', cell: ({ getValue }) => { return (
{getValue()}
); }, size: 64, sortingFn: 'basic', }), columnHelper.accessor('method', { header: 'Method', cell: ({ getValue }) =>
{getValue()}
, size: 64, sortingFn: 'alphanumeric', }), columnHelper.accessor('domain', { header: 'Domain', cell: ({ getValue }) => (
{getValue()}
), size: 128, sortingFn: 'alphanumeric', }), columnHelper.accessor('size', { header: 'Size', cell: ({ getValue }) => (
{getValue()}
), size: 80, sortingFn: sortSize, }), columnHelper.accessor('time', { header: 'Time', cell: ({ getValue }) => (
{getValue()}
), size: 80, sortingFn: sortTime, }), ]; export type RequestListProps = { requests: ProcessedRequest[]; }; export const RequestList = ({ requests: filteredRequests }: RequestListProps) => { const actions = useNetworkActivityActions(); const selectedRequestId = useSelectedRequestId(); const [sorting, setSorting] = useState([]); const overrides = useOverrides(); const clientUISettings = useClientUISettings(); const requests = useMemo(() => { return processNetworkRequests( filteredRequests, overrides, clientUISettings?.showUrlAsName, ); }, [filteredRequests, overrides, clientUISettings?.showUrlAsName]); const table = useReactTable({ data: requests, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, state: { sorting, }, }); const onRequestSelect = (requestId: RequestId): void => { actions.setSelectedRequest(requestId); }; return (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.map((row) => ( onRequestSelect(row.original.id)} > {row.getVisibleCells().map((cell) => ( ))} ))}
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} {header.column.getCanSort() && ( {{ asc: '↑', desc: '↓', }[header.column.getIsSorted() as string] ?? '↕'} )}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
); }; export { formatSize, formatDuration, formatStartTime, extractDomainAndPath, generateName, processNetworkRequests, };