Add CSV export functionality to HTTP requests table

Enables users to download filtered HTTP request data as CSV files with comprehensive data including timing, sizes, CDN analysis, and queue analysis.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-19 06:43:54 -05:00
parent 488d9a2650
commit a6a7bbb65b
4 changed files with 300 additions and 1 deletions

View File

@ -0,0 +1,129 @@
.container {
padding: 20px;
}
.errorContainer {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.errorMessage {
background: #fee;
border: 1px solid #fcc;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.errorMessage h3 {
color: #c33;
margin-bottom: 10px;
}
.exportControls {
display: flex;
justify-content: flex-end;
margin: 15px 0;
padding: 10px 0;
border-bottom: 1px solid #e0e0e0;
}
.downloadButton {
background: #4CAF50;
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.downloadButton:hover:not(:disabled) {
background: #45a049;
}
.downloadButton:disabled {
background: #cccccc;
cursor: not-allowed;
}
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modalContainer {
background: white;
border-radius: 12px;
width: 95vw;
height: 95vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
background: #f8f9fa;
}
.modalTitle {
margin: 0;
color: #333;
font-size: 18px;
}
.modalCloseButton {
background: #dc3545;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.modalCloseButton:hover {
background: #c82333;
}
.modalContent {
flex: 1;
padding: 0;
overflow: hidden;
}
.modalLegend {
background: #f8f9fa;
border-top: 1px solid #e0e0e0;
padding: 15px 20px;
font-size: 13px;
line-height: 1.5;
max-height: 200px;
overflow-y: auto;
}
.modalLegend div {
margin-bottom: 4px;
}

View File

@ -58,6 +58,7 @@ import type { HTTPRequest, ScreenshotEvent } from './types/httpRequest'
import HTTPRequestLoading from "./HTTPRequestLoading.tsx";
import sortRequests from "./lib/sortRequests.ts";
import PaginationControls from "./PaginationControls.tsx";
import { exportRequestsToCSV, downloadCSV } from './lib/csvExport';
interface HTTPRequestViewerProps {
@ -382,6 +383,15 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
setExpandedRows(newExpanded)
}
const handleDownloadCSV = () => {
const csvContent = exportRequestsToCSV(filteredRequests, {
includeHeaders: true
})
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
const filename = `http-requests-${timestamp}.csv`
downloadCSV(csvContent, filename)
}
if (loading) {
return (
@ -439,6 +449,17 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
handleSSIMRecalculate={handleSSIMRecalculate}
/>
{/* Export Controls */}
<div className={styles.exportControls}>
<button
onClick={handleDownloadCSV}
className={styles.downloadButton}
disabled={filteredRequests.length === 0}
>
📥 Download CSV ({filteredRequests.length} requests)
</button>
</div>
<PaginationControls currentPage={currentPage} setCurrentPage={setCurrentPage} totalPages={totalPages} />
{/* 3D Network Visualization Modal */}

View File

@ -214,7 +214,6 @@ const RequestsTable: React.FC<RequestsTableProps> = ({
<RequestRowSummary
key={request.requestId}
request={request}
allRequests={httpRequests}
showQueueAnalysis={showQueueAnalysis}
isExpanded={isExpanded}
onToggleRowExpansion={onToggleRowExpansion}

View File

@ -0,0 +1,150 @@
import type { HTTPRequest } from '../types/httpRequest'
export interface CSVExportOptions {
includeHeaders?: boolean
selectedColumns?: string[]
}
const DEFAULT_COLUMNS = [
'method',
'url',
'hostname',
'statusCode',
'resourceType',
'priority',
'protocol',
'connectionNumber',
'requestNumber',
'startTime',
'queueTime',
'dnsTime',
'connectionTime',
'serverLatency',
'duration',
'totalResponseTime',
'dataRate',
'size',
'contentLength',
'fromCache',
'connectionReused',
'cdnAnalysis.provider',
'queueAnalysis.reason'
]
// Format a value for CSV output
const formatCSVValue = (value: any): string => {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'number') {
return value.toString()
}
if (typeof value === 'boolean') {
return value ? 'true' : 'false'
}
const stringValue = value.toString()
// Escape quotes and wrap in quotes if contains comma, quote, or newline
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`
}
return stringValue
}
// Get nested property value from object
const getNestedValue = (obj: any, path: string): any => {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : null
}, obj)
}
// Convert HTTP requests to CSV format
export const exportRequestsToCSV = (
requests: HTTPRequest[],
options: CSVExportOptions = {}
): string => {
const {
includeHeaders = true,
selectedColumns = DEFAULT_COLUMNS
} = options
if (requests.length === 0) {
return includeHeaders ? selectedColumns.join(',') + '\n' : ''
}
const lines: string[] = []
// Add headers if requested
if (includeHeaders) {
const headers = selectedColumns.map(col => {
// Clean up column names for headers
return col
.replace(/([A-Z])/g, ' $1') // Add space before capitals
.replace(/^\w/, c => c.toUpperCase()) // Capitalize first letter
.replace(/\./g, ' ') // Replace dots with spaces
.trim()
})
lines.push(headers.map(formatCSVValue).join(','))
}
// Add data rows
for (const request of requests) {
const values = selectedColumns.map(column => {
const value = getNestedValue(request, column)
return formatCSVValue(value)
})
lines.push(values.join(','))
}
return lines.join('\n')
}
// Download CSV file
export const downloadCSV = (csvContent: string, filename: string = 'http-requests.csv'): void => {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
if (link.download !== undefined) {
// Use HTML5 download attribute
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', filename)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
}
// Get all available columns from HTTP requests
export const getAvailableColumns = (requests: HTTPRequest[]): string[] => {
if (requests.length === 0) {
return DEFAULT_COLUMNS
}
const columns = new Set<string>()
const extractColumns = (obj: any, prefix = ''): void => {
Object.keys(obj).forEach(key => {
const fullKey = prefix ? `${prefix}.${key}` : key
const value = obj[key]
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// Recursively extract nested properties
extractColumns(value, fullKey)
} else {
columns.add(fullKey)
}
})
}
// Extract columns from first request
extractColumns(requests[0])
return Array.from(columns).sort()
}