Add Request Breakdown page with comprehensive data analysis
Introduces new breakdown page that provides detailed statistics and visualizations for HTTP request data analysis across multiple dimensions. Features: - Overall statistics dashboard (total requests, size, success rate, cache hit rate) - Resource type breakdown with visual progress bars - Status code analysis by HTTP status categories - Hostname breakdown showing top 10 domains - Total and average response time metrics - Responsive design with mobile-friendly layouts - Added to main navigation as "Request Breakdown" tab 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
327ef29d55
commit
33fb2b1674
20
src/App.tsx
20
src/App.tsx
@ -4,13 +4,14 @@ import TraceViewer from './components/TraceViewer'
|
||||
import PhaseViewer from './components/PhaseViewer'
|
||||
import HTTPRequestViewer from './components/httprequestviewer/HTTPRequestViewer'
|
||||
import JavaScriptViewer from './components/javascriptviewer/JavaScriptViewer'
|
||||
import RequestBreakdown from './components/RequestBreakdown'
|
||||
import RequestDebugger from './components/RequestDebugger'
|
||||
import TraceUpload from './components/TraceUpload'
|
||||
import TraceSelector from './components/TraceSelector'
|
||||
import { traceDatabase } from './utils/traceDatabase'
|
||||
import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
|
||||
|
||||
type AppView = 'trace' | 'phases' | 'http' | 'js' | 'debug'
|
||||
type AppView = 'trace' | 'phases' | 'http' | 'js' | 'breakdown' | 'debug'
|
||||
type AppMode = 'selector' | 'upload' | 'analysis'
|
||||
type ThreeDView = 'network' | 'timeline' | null
|
||||
|
||||
@ -34,7 +35,7 @@ const getUrlParams = () => {
|
||||
const view = segments[1] as AppView
|
||||
const threeDView = segments[2] as ThreeDView
|
||||
// Validate view and 3D view values
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'debug']
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'breakdown', 'debug']
|
||||
const validThreeDViews: (ThreeDView)[] = ['network', 'timeline']
|
||||
const validatedView = validViews.includes(view) ? view : 'http'
|
||||
const validatedThreeDView = validThreeDViews.includes(threeDView) ? threeDView : null
|
||||
@ -43,7 +44,7 @@ const getUrlParams = () => {
|
||||
const traceId = segments[0]
|
||||
const view = segments[1] as AppView
|
||||
// Validate view is one of the allowed values
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'debug']
|
||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'breakdown', 'debug']
|
||||
const validatedView = validViews.includes(view) ? view : 'http'
|
||||
return { traceId, view: validatedView, threeDView: null }
|
||||
} else if (segments.length === 1) {
|
||||
@ -229,6 +230,15 @@ function App() {
|
||||
>
|
||||
JavaScript
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.navButton} ${currentView === 'breakdown' ? styles.active : ''}`}
|
||||
onClick={() => {
|
||||
setCurrentView('breakdown')
|
||||
updateUrlWithTraceId(selectedTraceId, 'breakdown', null)
|
||||
}}
|
||||
>
|
||||
Request Breakdown
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.navButton} ${currentView === 'debug' ? styles.active : ''}`}
|
||||
onClick={() => {
|
||||
@ -257,6 +267,10 @@ function App() {
|
||||
<JavaScriptViewer traceId={selectedTraceId} />
|
||||
)}
|
||||
|
||||
{currentView === 'breakdown' && (
|
||||
<RequestBreakdown traceId={selectedTraceId} />
|
||||
)}
|
||||
|
||||
{currentView === 'debug' && traceData && (
|
||||
<RequestDebugger traceEvents={traceData.traceEvents} />
|
||||
)}
|
||||
|
234
src/components/RequestBreakdown.module.css
Normal file
234
src/components/RequestBreakdown.module.css
Normal file
@ -0,0 +1,234 @@
|
||||
.container {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container h2 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error {
|
||||
flex-direction: column;
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error h3 {
|
||||
color: #c33;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Overall Stats Grid */
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.statCard:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.statCard h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Breakdown Sections */
|
||||
.breakdownSection {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.breakdownSection h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.breakdownTable {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr 1.5fr 1.5fr 1.5fr;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr 1.5fr 1.5fr 1.5fr;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tableRow:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tableRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.categoryName {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.percentageCell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.moreInfo {
|
||||
padding: 12px 20px;
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tableHeader, .tableRow {
|
||||
grid-template-columns: 1.5fr 0.8fr 0.8fr 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 12px 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.breakdownSection h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.container h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.statsGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tableHeader, .tableRow {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tableHeader span, .tableRow span, .tableRow .percentageCell {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.tableHeader span:before {
|
||||
content: attr(data-label) ": ";
|
||||
font-weight: 700;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tableRow span:before {
|
||||
content: attr(data-label) ": ";
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.categoryName:before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.percentageCell {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
height: 4px;
|
||||
}
|
||||
}
|
344
src/components/RequestBreakdown.tsx
Normal file
344
src/components/RequestBreakdown.tsx
Normal file
@ -0,0 +1,344 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData'
|
||||
import { processHTTPRequests } from './httprequestviewer/lib/httpRequestProcessor'
|
||||
import { addRequestPostProcessing } from './httprequestviewer/lib/requestPostProcessor'
|
||||
import { analyzeCDN, analyzeQueueReason } from './httprequestviewer/lib/analysisUtils'
|
||||
import { assignConnectionNumbers } from './httprequestviewer/lib/connectionUtils'
|
||||
import sortRequests from './httprequestviewer/lib/sortRequests'
|
||||
import type { HTTPRequest } from './httprequestviewer/types/httpRequest'
|
||||
import styles from './RequestBreakdown.module.css'
|
||||
|
||||
interface RequestBreakdownProps {
|
||||
traceId: string | null
|
||||
}
|
||||
|
||||
interface BreakdownStats {
|
||||
totalRequests: number
|
||||
totalSize: number
|
||||
totalDuration: number
|
||||
averageResponseTime: number
|
||||
successRate: number
|
||||
cacheHitRate: number
|
||||
}
|
||||
|
||||
interface CategoryBreakdown {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
totalSize: number
|
||||
averageResponseTime: number
|
||||
totalResponseTime: number
|
||||
}
|
||||
|
||||
const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
||||
const { traceData, loading, error } = useDatabaseTraceData(traceId)
|
||||
|
||||
const httpRequests = useMemo(() => {
|
||||
if (!traceData) return []
|
||||
const requests = processHTTPRequests(traceData.traceEvents)
|
||||
const sortedRequests = sortRequests(requests)
|
||||
const processedRequests = addRequestPostProcessing(sortedRequests, analyzeQueueReason, analyzeCDN)
|
||||
return assignConnectionNumbers(processedRequests)
|
||||
}, [traceData])
|
||||
|
||||
const overallStats: BreakdownStats = useMemo(() => {
|
||||
if (httpRequests.length === 0) {
|
||||
return {
|
||||
totalRequests: 0,
|
||||
totalSize: 0,
|
||||
totalDuration: 0,
|
||||
averageResponseTime: 0,
|
||||
successRate: 0,
|
||||
cacheHitRate: 0
|
||||
}
|
||||
}
|
||||
|
||||
const totalSize = httpRequests.reduce((sum, req) => {
|
||||
const size = req.contentLength || req.encodedDataLength || 0
|
||||
return sum + size
|
||||
}, 0)
|
||||
|
||||
const totalDuration = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.duration || 0)
|
||||
}, 0)
|
||||
|
||||
const totalResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
const successfulRequests = httpRequests.filter(req =>
|
||||
req.statusCode && req.statusCode >= 200 && req.statusCode < 300
|
||||
).length
|
||||
|
||||
const cachedRequests = httpRequests.filter(req => req.fromCache).length
|
||||
|
||||
return {
|
||||
totalRequests: httpRequests.length,
|
||||
totalSize,
|
||||
totalDuration,
|
||||
averageResponseTime: totalResponseTime / httpRequests.length,
|
||||
successRate: (successfulRequests / httpRequests.length) * 100,
|
||||
cacheHitRate: (cachedRequests / httpRequests.length) * 100
|
||||
}
|
||||
}, [httpRequests])
|
||||
|
||||
const resourceTypeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||
const typeMap = new Map<string, HTTPRequest[]>()
|
||||
|
||||
httpRequests.forEach(req => {
|
||||
const type = req.resourceType || 'unknown'
|
||||
if (!typeMap.has(type)) {
|
||||
typeMap.set(type, [])
|
||||
}
|
||||
typeMap.get(type)!.push(req)
|
||||
})
|
||||
|
||||
return Array.from(typeMap.entries()).map(([type, requests]) => {
|
||||
const totalSize = requests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalResponseTime = requests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
name: type,
|
||||
count: requests.length,
|
||||
percentage: (requests.length / httpRequests.length) * 100,
|
||||
totalSize,
|
||||
averageResponseTime: totalResponseTime / requests.length,
|
||||
totalResponseTime
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count)
|
||||
}, [httpRequests])
|
||||
|
||||
const statusCodeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||
const statusMap = new Map<string, HTTPRequest[]>()
|
||||
|
||||
httpRequests.forEach(req => {
|
||||
const status = req.statusCode ? Math.floor(req.statusCode / 100) + 'xx' : 'Unknown'
|
||||
if (!statusMap.has(status)) {
|
||||
statusMap.set(status, [])
|
||||
}
|
||||
statusMap.get(status)!.push(req)
|
||||
})
|
||||
|
||||
return Array.from(statusMap.entries()).map(([status, requests]) => {
|
||||
const totalSize = requests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalResponseTime = requests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
name: status,
|
||||
count: requests.length,
|
||||
percentage: (requests.length / httpRequests.length) * 100,
|
||||
totalSize,
|
||||
averageResponseTime: totalResponseTime / requests.length,
|
||||
totalResponseTime
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count)
|
||||
}, [httpRequests])
|
||||
|
||||
const hostnameBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||
const hostMap = new Map<string, HTTPRequest[]>()
|
||||
|
||||
httpRequests.forEach(req => {
|
||||
const hostname = req.hostname || 'unknown'
|
||||
if (!hostMap.has(hostname)) {
|
||||
hostMap.set(hostname, [])
|
||||
}
|
||||
hostMap.get(hostname)!.push(req)
|
||||
})
|
||||
|
||||
return Array.from(hostMap.entries()).map(([hostname, requests]) => {
|
||||
const totalSize = requests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalResponseTime = requests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
name: hostname,
|
||||
count: requests.length,
|
||||
percentage: (requests.length / httpRequests.length) * 100,
|
||||
totalSize,
|
||||
averageResponseTime: totalResponseTime / requests.length,
|
||||
totalResponseTime
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count)
|
||||
}, [httpRequests])
|
||||
|
||||
const formatSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const formatDuration = (microseconds: number): string => {
|
||||
if (microseconds < 1000) return `${microseconds.toFixed(0)} μs`
|
||||
if (microseconds < 1000000) return `${(microseconds / 1000).toFixed(1)} ms`
|
||||
return `${(microseconds / 1000000).toFixed(2)} s`
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.loading}>Loading request breakdown...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.error}>
|
||||
<h3>Error Loading Data</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2>Request Data Breakdown</h2>
|
||||
|
||||
{/* Overall Stats */}
|
||||
<div className={styles.statsGrid}>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Total Requests</h3>
|
||||
<div className={styles.statValue}>{overallStats.totalRequests}</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Total Size</h3>
|
||||
<div className={styles.statValue}>{formatSize(overallStats.totalSize)}</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Average Response Time</h3>
|
||||
<div className={styles.statValue}>{formatDuration(overallStats.averageResponseTime)}</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Success Rate</h3>
|
||||
<div className={styles.statValue}>{overallStats.successRate.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Cache Hit Rate</h3>
|
||||
<div className={styles.statValue}>{overallStats.cacheHitRate.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resource Type Breakdown */}
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3>By Resource Type</h3>
|
||||
<div className={styles.breakdownTable}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span>Type</span>
|
||||
<span>Count</span>
|
||||
<span>Percentage</span>
|
||||
<span>Total Size</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>Avg Response Time</span>
|
||||
</div>
|
||||
{resourceTypeBreakdown.map(item => (
|
||||
<div key={item.name} className={styles.tableRow}>
|
||||
<span className={styles.categoryName}>{item.name}</span>
|
||||
<span>{item.count}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.percentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatSize(item.totalSize)}</span>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Code Breakdown */}
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3>By Status Code</h3>
|
||||
<div className={styles.breakdownTable}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span>Status</span>
|
||||
<span>Count</span>
|
||||
<span>Percentage</span>
|
||||
<span>Total Size</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>Avg Response Time</span>
|
||||
</div>
|
||||
{statusCodeBreakdown.map(item => (
|
||||
<div key={item.name} className={styles.tableRow}>
|
||||
<span className={styles.categoryName}>{item.name}</span>
|
||||
<span>{item.count}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.percentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatSize(item.totalSize)}</span>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hostname Breakdown */}
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3>By Hostname</h3>
|
||||
<div className={styles.breakdownTable}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span>Hostname</span>
|
||||
<span>Count</span>
|
||||
<span>Percentage</span>
|
||||
<span>Total Size</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>Avg Response Time</span>
|
||||
</div>
|
||||
{hostnameBreakdown.slice(0, 10).map(item => (
|
||||
<div key={item.name} className={styles.tableRow}>
|
||||
<span className={styles.categoryName}>{item.name}</span>
|
||||
<span>{item.count}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.percentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatSize(item.totalSize)}</span>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{hostnameBreakdown.length > 10 && (
|
||||
<div className={styles.moreInfo}>
|
||||
Showing top 10 of {hostnameBreakdown.length} hostnames
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RequestBreakdown
|
Loading…
Reference in New Issue
Block a user