diff --git a/src/components/httprequestviewer/ColumnSettings.module.css b/src/components/httprequestviewer/ColumnSettings.module.css new file mode 100644 index 0000000..a96fe29 --- /dev/null +++ b/src/components/httprequestviewer/ColumnSettings.module.css @@ -0,0 +1,125 @@ +/* Column Settings component styles using CSS variables from App.module.css */ + +.columnSettings { + margin-bottom: var(--spacing-md); +} + +.toggleButton { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + color: var(--color-text); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-md); + cursor: pointer; + font-size: var(--font-size-base); + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.toggleButton:hover { + background: var(--color-bg-hover); + border-color: var(--color-primary); + color: var(--color-primary); +} + +.panel { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--spacing-lg); + margin-top: var(--spacing-sm); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.panelHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.panelHeader h3 { + margin: 0; + color: var(--color-text-highlight); + font-size: var(--font-size-lg); + font-weight: bold; +} + +.bulkActions { + display: flex; + gap: var(--spacing-xs); +} + +.bulkButton { + background: var(--color-bg-light); + border: 1px solid var(--color-border); + color: var(--color-text); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-sm); + cursor: pointer; + font-size: var(--font-size-sm); + transition: all 0.2s ease; +} + +.bulkButton:hover { + background: var(--color-primary); + color: white; + border-color: var(--color-primary); +} + +.columnGroups { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-lg); +} + +.columnGroup { + background: var(--color-bg-light); + border-radius: var(--radius-md); + padding: var(--spacing-md); +} + +.groupTitle { + margin: 0 0 var(--spacing-md) 0; + color: var(--color-text-highlight); + font-size: var(--font-size-md); + font-weight: bold; + border-bottom: 1px solid var(--color-border); + padding-bottom: var(--spacing-xs); +} + +.columnList { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.columnItem { + display: flex; + align-items: center; + gap: var(--spacing-xs); + cursor: pointer; + padding: var(--spacing-xs); + border-radius: var(--radius-sm); + transition: background-color 0.2s ease; +} + +.columnItem:hover { + background-color: var(--color-bg-hover); +} + +.checkbox { + accent-color: var(--color-primary); + cursor: pointer; +} + +.columnLabel { + font-size: var(--font-size-base); + color: var(--color-text); + cursor: pointer; + user-select: none; +} \ No newline at end of file diff --git a/src/components/httprequestviewer/ColumnSettings.tsx b/src/components/httprequestviewer/ColumnSettings.tsx new file mode 100644 index 0000000..f2fb8d8 --- /dev/null +++ b/src/components/httprequestviewer/ColumnSettings.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import styles from './ColumnSettings.module.css' + +interface ColumnConfig { + key: string + label: string + group: string +} + +const COLUMN_CONFIGS: ColumnConfig[] = [ + // Basic Info + { key: 'expand', label: 'Expand', group: 'Basic' }, + { key: 'method', label: 'Method', group: 'Basic' }, + { key: 'status', label: 'Status', group: 'Basic' }, + { key: 'type', label: 'Type', group: 'Basic' }, + { key: 'priority', label: 'Priority', group: 'Basic' }, + { key: 'url', label: 'URL', group: 'Basic' }, + + // Connection Info + { key: 'connectionNumber', label: 'Connection #', group: 'Connection' }, + { key: 'requestNumber', label: 'Request #', group: 'Connection' }, + + // Timing + { key: 'startTime', label: 'Start Time', group: 'Timing' }, + { key: 'queueTime', label: 'Queue Time', group: 'Timing' }, + { key: 'dns', label: 'DNS', group: 'Timing' }, + { key: 'connection', label: 'Connection', group: 'Timing' }, + { key: 'serverLatency', label: 'Server Latency', group: 'Timing' }, + { key: 'duration', label: 'Duration', group: 'Timing' }, + { key: 'totalResponseTime', label: 'Total Response Time', group: 'Timing' }, + + // Size & Performance + { key: 'dataRate', label: 'Data Rate', group: 'Performance' }, + { key: 'size', label: 'Size', group: 'Performance' }, + { key: 'contentLength', label: 'Content-Length', group: 'Performance' }, + + // Advanced + { key: 'protocol', label: 'Protocol', group: 'Advanced' }, + { key: 'cdn', label: 'CDN', group: 'Advanced' }, + { key: 'cache', label: 'Cache', group: 'Advanced' } +] + +interface ColumnSettingsProps { + visibleColumns: Record + onColumnToggle: (column: string) => void + isOpen: boolean + onToggle: () => void + onShowAll: () => void + onHideAll: () => void + onResetDefaults: () => void +} + +const ColumnSettings: React.FC = ({ + visibleColumns, + onColumnToggle, + isOpen, + onToggle, + onShowAll, + onHideAll, + onResetDefaults +}) => { + const groups = [...new Set(COLUMN_CONFIGS.map(col => col.group))] + + return ( +
+ + + {isOpen && ( +
+
+

Column Visibility

+
+ + + +
+
+ +
+ {groups.map(group => ( +
+

{group}

+
+ {COLUMN_CONFIGS + .filter(col => col.group === group) + .map(column => ( + + )) + } +
+
+ ))} +
+
+ )} +
+ ) +} + +export default ColumnSettings \ No newline at end of file diff --git a/src/components/httprequestviewer/HTTPRequestViewer.tsx b/src/components/httprequestviewer/HTTPRequestViewer.tsx index 8ef8790..253a135 100644 --- a/src/components/httprequestviewer/HTTPRequestViewer.tsx +++ b/src/components/httprequestviewer/HTTPRequestViewer.tsx @@ -3,6 +3,7 @@ import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData' import { getUrlParams, updateUrlWithTraceId } from '../../App' import RequestFilters from './RequestFilters' import RequestsTable from './RequestsTable' +import ColumnSettings from './ColumnSettings' import styles from './HTTPRequestViewer.module.css' // Lazy load 3D viewers to reduce main bundle size @@ -76,6 +77,43 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) { // 3D viewer state - initialized from URL const [show3DViewer, setShow3DViewer] = useState(false) const [showTimelineViewer, setShowTimelineViewer] = useState(false) + + // Column visibility state - default visible columns as requested + const [visibleColumns, setVisibleColumns] = useState(() => { + const saved = localStorage.getItem('httpRequestTableColumns') + if (saved) { + try { + return JSON.parse(saved) + } catch (e) { + console.warn('Failed to parse saved column settings, using defaults') + } + } + // Default visible columns + return { + expand: true, + method: false, + status: false, + type: false, + priority: false, + url: true, + connectionNumber: false, + requestNumber: false, + startTime: true, + queueTime: false, + dns: false, + connection: false, + serverLatency: false, + duration: false, + totalResponseTime: true, + dataRate: true, + size: false, + contentLength: true, + protocol: false, + cdn: false, + cache: false + } + }) + const [columnSettingsOpen, setColumnSettingsOpen] = useState(false) // Initialize 3D view state from URL on component mount and handle URL changes useEffect(() => { @@ -117,6 +155,62 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) { setSsimThreshold(pendingSSIMThreshold) } + // Column visibility handlers + const handleColumnToggle = (column: string) => { + const newVisibleColumns = { + ...visibleColumns, + [column]: !visibleColumns[column] + } + setVisibleColumns(newVisibleColumns) + localStorage.setItem('httpRequestTableColumns', JSON.stringify(newVisibleColumns)) + } + + const handleShowAllColumns = () => { + const allVisible = Object.keys(visibleColumns).reduce((acc, key) => { + acc[key] = true + return acc + }, {} as Record) + setVisibleColumns(allVisible) + localStorage.setItem('httpRequestTableColumns', JSON.stringify(allVisible)) + } + + const handleHideAllColumns = () => { + const allHidden = Object.keys(visibleColumns).reduce((acc, key) => { + acc[key] = key === 'expand' // Always keep expand column visible + return acc + }, {} as Record) + setVisibleColumns(allHidden) + localStorage.setItem('httpRequestTableColumns', JSON.stringify(allHidden)) + } + + const handleResetDefaults = () => { + const defaultColumns = { + expand: true, + method: false, + status: false, + type: false, + priority: false, + url: true, + connectionNumber: false, + requestNumber: false, + startTime: true, + queueTime: false, + dns: false, + connection: false, + serverLatency: false, + duration: false, + totalResponseTime: true, + dataRate: true, + size: false, + contentLength: true, + protocol: false, + cdn: false, + cache: false + } + setVisibleColumns(defaultColumns) + localStorage.setItem('httpRequestTableColumns', JSON.stringify(defaultColumns)) + } + const httpRequests = useMemo(() => { if (!traceData) return [] const httpRequests = processHTTPRequests(traceData.traceEvents) @@ -429,6 +523,17 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) { )} + {/* Column Settings */} + setColumnSettingsOpen(!columnSettingsOpen)} + onShowAll={handleShowAllColumns} + onHideAll={handleHideAllColumns} + onResetDefaults={handleResetDefaults} + /> + {/* Requests Table */} ) diff --git a/src/components/httprequestviewer/RequestRowDetails.tsx b/src/components/httprequestviewer/RequestRowDetails.tsx index 5964dbc..723a1f9 100644 --- a/src/components/httprequestviewer/RequestRowDetails.tsx +++ b/src/components/httprequestviewer/RequestRowDetails.tsx @@ -14,12 +14,15 @@ import { interface RequestRowDetailsProps { request: HTTPRequest + visibleColumns: Record } -const RequestRowDetails: React.FC = ({ request }) => { +const RequestRowDetails: React.FC = ({ request, visibleColumns }) => { + // Calculate the number of visible columns for colSpan + const visibleColumnCount = Object.values(visibleColumns).filter(Boolean).length return ( - +
{/* Request Details */} diff --git a/src/components/httprequestviewer/RequestRowSummary.tsx b/src/components/httprequestviewer/RequestRowSummary.tsx index 4b5fa61..f165f67 100644 --- a/src/components/httprequestviewer/RequestRowSummary.tsx +++ b/src/components/httprequestviewer/RequestRowSummary.tsx @@ -25,125 +25,174 @@ interface RequestRowSummaryProps { showQueueAnalysis: boolean isExpanded: boolean onToggleRowExpansion: (requestId: string) => void + visibleColumns: Record } const RequestRowSummary: React.FC = ({ request, showQueueAnalysis, isExpanded, - onToggleRowExpansion + onToggleRowExpansion, + visibleColumns }) => { return ( <> onToggleRowExpansion(request.requestId)} > - - {isExpanded ? '−' : '+'} - - - {request.method} - - - {request.statusCode || '-'} - - - {request.resourceType} - - - - {getPriorityIcon(request.priority)} - {request.priority || '-'} - - - - - {truncateUrl(request.url)} - - - - {request.connectionNumber || '-'} - - - {request.requestNumberOnConnection || '-'} - - - {formatDuration(request.timing.startOffset)} - - -
- {formatDuration(request.timing.queueTime)} - {showQueueAnalysis && request.queueAnalysis && ( - - {getQueueAnalysisIcon(request.queueAnalysis)} - - )} -
- + {visibleColumns.expand && ( + + {isExpanded ? '−' : '+'} + + )} + {visibleColumns.method && ( + + {request.method} + + )} + {visibleColumns.status && ( + + {request.statusCode || '-'} + + )} + {visibleColumns.type && ( + + {request.resourceType} + + )} + {visibleColumns.priority && ( + + + {getPriorityIcon(request.priority)} + {request.priority || '-'} + + + )} + {visibleColumns.url && ( + + + {truncateUrl(request.url)} + + + )} + {visibleColumns.connectionNumber && ( + + {request.connectionNumber || '-'} + + )} + {visibleColumns.requestNumber && ( + + {request.requestNumberOnConnection || '-'} + + )} + {visibleColumns.startTime && ( + + {formatDuration(request.timing.startOffset)} + + )} + {visibleColumns.queueTime && ( + +
+ {formatDuration(request.timing.queueTime)} + {showQueueAnalysis && request.queueAnalysis && ( + + {getQueueAnalysisIcon(request.queueAnalysis)} + + )} +
+ + )} {/* DNS Time */} - - {request.timing.dnsStart !== undefined && request.timing.dnsEnd !== undefined && request.timing.dnsStart >= 0 && request.timing.dnsEnd >= 0 - ? formatDuration((request.timing.dnsEnd || 0) - (request.timing.dnsStart || 0)) - : cached - } - + {visibleColumns.dns && ( + + {request.timing.dnsStart !== undefined && request.timing.dnsEnd !== undefined && request.timing.dnsStart >= 0 && request.timing.dnsEnd >= 0 + ? formatDuration((request.timing.dnsEnd || 0) - (request.timing.dnsStart || 0)) + : cached + } + + )} {/* Connection Time */} - - {request.timing.connectStart !== undefined && request.timing.connectEnd !== undefined - && request.timing.connectStart >= 0 && request.timing.connectEnd >= 0 - ? - {formatDuration((request.timing.connectEnd || 0) - (request.timing.connectStart || 0))} - - : - {request.connectionReused ? 'reused' : 'cached'} - - } - - - {formatDuration(request.timing.serverLatency)} - + {visibleColumns.connection && ( + + {request.timing.connectStart !== undefined && request.timing.connectEnd !== undefined + && request.timing.connectStart >= 0 && request.timing.connectEnd >= 0 + ? + {formatDuration((request.timing.connectEnd || 0) - (request.timing.connectStart || 0))} + + : + {request.connectionReused ? 'reused' : 'cached'} + + } + + )} - - {formatDuration(request.timing.duration)} - + {visibleColumns.serverLatency && ( + + {formatDuration(request.timing.serverLatency)} + + )} + + {visibleColumns.duration && ( + + {formatDuration(request.timing.duration)} + + )} {/* Total Response Time */} - - {formatDuration(request.timing.totalResponseTime)} - + {visibleColumns.totalResponseTime && ( + + {formatDuration(request.timing.totalResponseTime)} + + )} {/* Data Rate */} - - {formatDataRate(request.encodedDataLength, request.contentLength, request.timing.duration)} - + {visibleColumns.dataRate && ( + + {formatDataRate(request.encodedDataLength, request.contentLength, request.timing.duration)} + + )} - - {formatSize(request.encodedDataLength)} - - - {request.contentLength ? formatSize(request.contentLength) : '-'} - - - {request.protocol || '-'} - - - {request.cdnAnalysis ? getCDNIcon(request.cdnAnalysis) : '-'} - - - {request.fromCache ? '💾' : request.connectionReused ? '🔄' : '🌐'} - + {visibleColumns.size && ( + + {formatSize(request.encodedDataLength)} + + )} + + {visibleColumns.contentLength && ( + + {request.contentLength ? formatSize(request.contentLength) : '-'} + + )} + + {visibleColumns.protocol && ( + + {request.protocol || '-'} + + )} + + {visibleColumns.cdn && ( + + {request.cdnAnalysis ? getCDNIcon(request.cdnAnalysis) : '-'} + + )} + + {visibleColumns.cache && ( + + {request.fromCache ? '💾' : request.connectionReused ? '🔄' : '🌐'} + + )} {/* Expanded Row Details */} - {isExpanded && } + {isExpanded && } ) } diff --git a/src/components/httprequestviewer/RequestsTable.tsx b/src/components/httprequestviewer/RequestsTable.tsx index 01bad2a..c477489 100644 --- a/src/components/httprequestviewer/RequestsTable.tsx +++ b/src/components/httprequestviewer/RequestsTable.tsx @@ -19,6 +19,7 @@ interface RequestsTableProps { // Display options showQueueAnalysis: boolean + visibleColumns: Record // Row expansion state expandedRows: Set @@ -31,6 +32,7 @@ const RequestsTable: React.FC = ({ paginatedTimelineEntries, paginatedRequests, showQueueAnalysis, + visibleColumns, expandedRows, onToggleRowExpansion }) => { @@ -41,111 +43,153 @@ const RequestsTable: React.FC = ({ - - - - - - - - - - - - - - - - - - - - - + {visibleColumns.expand && ( + + )} + {visibleColumns.method && ( + + )} + {visibleColumns.status && ( + + )} + {visibleColumns.type && ( + + )} + {visibleColumns.priority && ( + + )} + {visibleColumns.url && ( + + )} + {visibleColumns.connectionNumber && ( + + )} + {visibleColumns.requestNumber && ( + + )} + {visibleColumns.startTime && ( + + )} + {visibleColumns.queueTime && ( + + )} + {visibleColumns.dns && ( + + )} + {visibleColumns.connection && ( + + )} + {visibleColumns.serverLatency && ( + + )} + {visibleColumns.duration && ( + + )} + {visibleColumns.totalResponseTime && ( + + )} + {visibleColumns.dataRate && ( + + )} + {visibleColumns.size && ( + + )} + {visibleColumns.contentLength && ( + + )} + {visibleColumns.protocol && ( + + )} + {visibleColumns.cdn && ( + + )} + {visibleColumns.cache && ( + + )} @@ -173,6 +217,7 @@ const RequestsTable: React.FC = ({ showQueueAnalysis={showQueueAnalysis} isExpanded={isExpanded} onToggleRowExpansion={onToggleRowExpansion} + visibleColumns={visibleColumns} /> ) })}
- - Expand - - - - Method - - - - Status - - - - Type - - - - Priority - - - - URL - - - - Connection # - - - - Request # - - - - Start Time - - - - Queue Time - - - - DNS - - - - Connection - - - - Server Latency - - - - Duration - - - - Total Response Time - - - - Data Rate - - - - Size - - - - Content-Length - - - - Protocol - - - - CDN - - - - Cache - - + + Expand + + + + Method + + + + Status + + + + Type + + + + Priority + + + + URL + + + + Connection # + + + + Request # + + + + Start Time + + + + Queue Time + + + + DNS + + + + Connection + + + + Server Latency + + + + Duration + + + + Total Response Time + + + + Data Rate + + + + Size + + + + Content-Length + + + + Protocol + + + + CDN + + + + Cache + +