Add comprehensive column sorting with icons and tooltips to RequestBreakdown
Enhance all breakdown sections (Resource Type, Status Code, Hostname) with: - Clickable column headers for sorting by any field - Visual sort indicators: neutral (⇅), ascending (▲), descending (▼) - Interactive tooltips showing current sort state and available actions - Default sort by request count in descending order - Toggle between ascending/descending by clicking same column - Consistent styling with brand colors and hover effects - Proper flexbox layout for icon positioning All 8 columns are sortable: name, count, count %, size, size %, response time, response time %, and average response time. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1035b28472
commit
b5b7aafca5
@ -1,21 +1,121 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styles from './RequestBreakdown.module.css'
|
import styles from './RequestBreakdown.module.css'
|
||||||
|
|
||||||
|
export type SortColumn = 'name' | 'count' | 'percentage' | 'totalSize' | 'sizePercentage' | 'totalResponseTime' | 'responseTimePercentage' | 'averageResponseTime'
|
||||||
|
export type SortDirection = 'asc' | 'desc'
|
||||||
|
|
||||||
interface BreakdownTableHeaderProps {
|
interface BreakdownTableHeaderProps {
|
||||||
categoryLabel: string
|
categoryLabel: string
|
||||||
|
sortColumn: SortColumn | null
|
||||||
|
sortDirection: SortDirection
|
||||||
|
onSort: (column: SortColumn) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreakdownTableHeader: React.FC<BreakdownTableHeaderProps> = ({ categoryLabel }) => {
|
const BreakdownTableHeader: React.FC<BreakdownTableHeaderProps> = ({
|
||||||
|
categoryLabel,
|
||||||
|
sortColumn,
|
||||||
|
sortDirection,
|
||||||
|
onSort
|
||||||
|
}) => {
|
||||||
|
const getSortIcon = (column: SortColumn) => {
|
||||||
|
if (sortColumn !== column) {
|
||||||
|
return <span className={styles.sortIconNeutral}>⇅</span>
|
||||||
|
}
|
||||||
|
return sortDirection === 'asc' ?
|
||||||
|
<span className={styles.sortIconAsc}>▲</span> :
|
||||||
|
<span className={styles.sortIconDesc}>▼</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTooltipText = (column: SortColumn, label: string) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
const oppositeDirection = sortDirection === 'asc' ? 'descending' : 'ascending'
|
||||||
|
return `Click to sort ${label.toLowerCase()} ${oppositeDirection}`
|
||||||
|
}
|
||||||
|
return `Click to sort by ${label.toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableHeader}>
|
<div className={styles.tableHeader}>
|
||||||
<span>{categoryLabel}</span>
|
<button
|
||||||
<span>Request Count</span>
|
className={styles.sortButton}
|
||||||
<span>Request Count %</span>
|
onClick={() => onSort('name')}
|
||||||
<span>Total Size</span>
|
title={getTooltipText('name', categoryLabel)}
|
||||||
<span>Total Size %</span>
|
>
|
||||||
<span>Total Response Time</span>
|
<span className={styles.sortButtonContent}>
|
||||||
<span>Total Response Time %</span>
|
{categoryLabel}
|
||||||
<span>Avg Response Time</span>
|
{getSortIcon('name')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('count')}
|
||||||
|
title={getTooltipText('count', 'Request Count')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Request Count
|
||||||
|
{getSortIcon('count')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('percentage')}
|
||||||
|
title={getTooltipText('percentage', 'Request Count %')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Request Count %
|
||||||
|
{getSortIcon('percentage')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('totalSize')}
|
||||||
|
title={getTooltipText('totalSize', 'Total Size')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Total Size
|
||||||
|
{getSortIcon('totalSize')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('sizePercentage')}
|
||||||
|
title={getTooltipText('sizePercentage', 'Total Size %')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Total Size %
|
||||||
|
{getSortIcon('sizePercentage')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('totalResponseTime')}
|
||||||
|
title={getTooltipText('totalResponseTime', 'Total Response Time')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Total Response Time
|
||||||
|
{getSortIcon('totalResponseTime')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('responseTimePercentage')}
|
||||||
|
title={getTooltipText('responseTimePercentage', 'Total Response Time %')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Total Response Time %
|
||||||
|
{getSortIcon('responseTimePercentage')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => onSort('averageResponseTime')}
|
||||||
|
title={getTooltipText('averageResponseTime', 'Average Response Time')}
|
||||||
|
>
|
||||||
|
<span className={styles.sortButtonContent}>
|
||||||
|
Avg Response Time
|
||||||
|
{getSortIcon('averageResponseTime')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
||||||
import BreakdownTableHeader from './BreakdownTableHeader'
|
import BreakdownTableHeader, { type SortColumn, type SortDirection } from './BreakdownTableHeader'
|
||||||
import styles from './RequestBreakdown.module.css'
|
import styles from './RequestBreakdown.module.css'
|
||||||
|
|
||||||
interface CategoryBreakdown {
|
interface CategoryBreakdown {
|
||||||
@ -20,6 +20,8 @@ interface HostnameBreakdownProps {
|
|||||||
|
|
||||||
const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) => {
|
const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) => {
|
||||||
const [showAllHostnames, setShowAllHostnames] = useState(false)
|
const [showAllHostnames, setShowAllHostnames] = useState(false)
|
||||||
|
const [sortColumn, setSortColumn] = useState<SortColumn | null>('count')
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
||||||
|
|
||||||
const formatSize = (bytes: number): string => {
|
const formatSize = (bytes: number): string => {
|
||||||
if (bytes < 1024) return `${bytes} B`
|
if (bytes < 1024) return `${bytes} B`
|
||||||
@ -33,7 +35,7 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
|
|||||||
return `${(microseconds / 1000000).toFixed(2)} s`
|
return `${(microseconds / 1000000).toFixed(2)} s`
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostnameBreakdown: CategoryBreakdown[] = useMemo(() => {
|
const baseHostnameBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||||
const hostMap = new Map<string, HTTPRequest[]>()
|
const hostMap = new Map<string, HTTPRequest[]>()
|
||||||
|
|
||||||
httpRequests.forEach(req => {
|
httpRequests.forEach(req => {
|
||||||
@ -72,15 +74,83 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
|
|||||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||||
averageResponseTime: totalResponseTime / requests.length
|
averageResponseTime: totalResponseTime / requests.length
|
||||||
}
|
}
|
||||||
}).sort((a, b) => b.count - a.count)
|
})
|
||||||
}, [httpRequests])
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const handleSort = (column: SortColumn) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortColumn(column)
|
||||||
|
setSortDirection('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedHostnameBreakdown = useMemo(() => {
|
||||||
|
if (!sortColumn) return baseHostnameBreakdown
|
||||||
|
|
||||||
|
return [...baseHostnameBreakdown].sort((a, b) => {
|
||||||
|
let aValue: string | number
|
||||||
|
let bValue: string | number
|
||||||
|
|
||||||
|
switch (sortColumn) {
|
||||||
|
case 'name':
|
||||||
|
aValue = a.name
|
||||||
|
bValue = b.name
|
||||||
|
break
|
||||||
|
case 'count':
|
||||||
|
aValue = a.count
|
||||||
|
bValue = b.count
|
||||||
|
break
|
||||||
|
case 'percentage':
|
||||||
|
aValue = a.percentage
|
||||||
|
bValue = b.percentage
|
||||||
|
break
|
||||||
|
case 'totalSize':
|
||||||
|
aValue = a.totalSize
|
||||||
|
bValue = b.totalSize
|
||||||
|
break
|
||||||
|
case 'sizePercentage':
|
||||||
|
aValue = a.sizePercentage
|
||||||
|
bValue = b.sizePercentage
|
||||||
|
break
|
||||||
|
case 'totalResponseTime':
|
||||||
|
aValue = a.totalResponseTime
|
||||||
|
bValue = b.totalResponseTime
|
||||||
|
break
|
||||||
|
case 'responseTimePercentage':
|
||||||
|
aValue = a.responseTimePercentage
|
||||||
|
bValue = b.responseTimePercentage
|
||||||
|
break
|
||||||
|
case 'averageResponseTime':
|
||||||
|
aValue = a.averageResponseTime
|
||||||
|
bValue = b.averageResponseTime
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
const comparison = aValue.localeCompare(bValue)
|
||||||
|
return sortDirection === 'asc' ? comparison : -comparison
|
||||||
|
} else {
|
||||||
|
const comparison = (aValue as number) - (bValue as number)
|
||||||
|
return sortDirection === 'asc' ? comparison : -comparison
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [baseHostnameBreakdown, sortColumn, sortDirection])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.breakdownSection}>
|
<div className={styles.breakdownSection}>
|
||||||
<h3>By Hostname</h3>
|
<h3>By Hostname</h3>
|
||||||
<div className={styles.breakdownTable}>
|
<div className={styles.breakdownTable}>
|
||||||
<BreakdownTableHeader categoryLabel="Hostname" />
|
<BreakdownTableHeader
|
||||||
{(showAllHostnames ? hostnameBreakdown : hostnameBreakdown.slice(0, 10)).map(item => (
|
categoryLabel="Hostname"
|
||||||
|
sortColumn={sortColumn}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
/>
|
||||||
|
{(showAllHostnames ? sortedHostnameBreakdown : sortedHostnameBreakdown.slice(0, 10)).map(item => (
|
||||||
<div key={item.name} className={styles.tableRow}>
|
<div key={item.name} className={styles.tableRow}>
|
||||||
<span className={styles.categoryName}>{item.name}</span>
|
<span className={styles.categoryName}>{item.name}</span>
|
||||||
<span>{item.count}</span>
|
<span>{item.count}</span>
|
||||||
@ -117,11 +187,11 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{hostnameBreakdown.length > 10 && (
|
{sortedHostnameBreakdown.length > 10 && (
|
||||||
<div className={styles.moreInfo}>
|
<div className={styles.moreInfo}>
|
||||||
{showAllHostnames ? (
|
{showAllHostnames ? (
|
||||||
<>
|
<>
|
||||||
Showing all {hostnameBreakdown.length} hostnames
|
Showing all {sortedHostnameBreakdown.length} hostnames
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAllHostnames(false)}
|
onClick={() => setShowAllHostnames(false)}
|
||||||
className={styles.toggleButton}
|
className={styles.toggleButton}
|
||||||
@ -131,7 +201,7 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Showing top 10 of {hostnameBreakdown.length} hostnames
|
Showing top 10 of {sortedHostnameBreakdown.length} hostnames
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAllHostnames(true)}
|
onClick={() => setShowAllHostnames(true)}
|
||||||
className={styles.toggleButton}
|
className={styles.toggleButton}
|
||||||
|
@ -107,6 +107,61 @@
|
|||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sortButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-transform: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortButton:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortButtonContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortIconNeutral,
|
||||||
|
.sortIconAsc,
|
||||||
|
.sortIconDesc {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortIconNeutral {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortIconAsc,
|
||||||
|
.sortIconDesc {
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortButton:hover .sortIconNeutral {
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortButton:hover .sortIconAsc,
|
||||||
|
.sortButton:hover .sortIconDesc {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.tableRow {
|
.tableRow {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr 1fr 1.5fr 1fr 1.5fr 1fr 1.5fr;
|
grid-template-columns: 2fr 1fr 1fr 1.5fr 1fr 1.5fr 1fr 1.5fr;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
||||||
import BreakdownTableHeader from './BreakdownTableHeader'
|
import BreakdownTableHeader, { type SortColumn, type SortDirection } from './BreakdownTableHeader'
|
||||||
import styles from './RequestBreakdown.module.css'
|
import styles from './RequestBreakdown.module.css'
|
||||||
|
|
||||||
interface CategoryBreakdown {
|
interface CategoryBreakdown {
|
||||||
@ -19,6 +19,8 @@ interface ResourceTypeBreakdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ httpRequests }) => {
|
const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ httpRequests }) => {
|
||||||
|
const [sortColumn, setSortColumn] = useState<SortColumn | null>('count')
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
||||||
const formatSize = (bytes: number): string => {
|
const formatSize = (bytes: number): string => {
|
||||||
if (bytes < 1024) return `${bytes} B`
|
if (bytes < 1024) return `${bytes} B`
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
@ -31,7 +33,7 @@ const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ httpReque
|
|||||||
return `${(microseconds / 1000000).toFixed(2)} s`
|
return `${(microseconds / 1000000).toFixed(2)} s`
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceTypeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
const baseResourceTypeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||||
const typeMap = new Map<string, HTTPRequest[]>()
|
const typeMap = new Map<string, HTTPRequest[]>()
|
||||||
|
|
||||||
httpRequests.forEach(req => {
|
httpRequests.forEach(req => {
|
||||||
@ -70,15 +72,83 @@ const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ httpReque
|
|||||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||||
averageResponseTime: totalResponseTime / requests.length
|
averageResponseTime: totalResponseTime / requests.length
|
||||||
}
|
}
|
||||||
}).sort((a, b) => b.count - a.count)
|
})
|
||||||
}, [httpRequests])
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const handleSort = (column: SortColumn) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortColumn(column)
|
||||||
|
setSortDirection('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedResourceTypeBreakdown = useMemo(() => {
|
||||||
|
if (!sortColumn) return baseResourceTypeBreakdown
|
||||||
|
|
||||||
|
return [...baseResourceTypeBreakdown].sort((a, b) => {
|
||||||
|
let aValue: string | number
|
||||||
|
let bValue: string | number
|
||||||
|
|
||||||
|
switch (sortColumn) {
|
||||||
|
case 'name':
|
||||||
|
aValue = a.name
|
||||||
|
bValue = b.name
|
||||||
|
break
|
||||||
|
case 'count':
|
||||||
|
aValue = a.count
|
||||||
|
bValue = b.count
|
||||||
|
break
|
||||||
|
case 'percentage':
|
||||||
|
aValue = a.percentage
|
||||||
|
bValue = b.percentage
|
||||||
|
break
|
||||||
|
case 'totalSize':
|
||||||
|
aValue = a.totalSize
|
||||||
|
bValue = b.totalSize
|
||||||
|
break
|
||||||
|
case 'sizePercentage':
|
||||||
|
aValue = a.sizePercentage
|
||||||
|
bValue = b.sizePercentage
|
||||||
|
break
|
||||||
|
case 'totalResponseTime':
|
||||||
|
aValue = a.totalResponseTime
|
||||||
|
bValue = b.totalResponseTime
|
||||||
|
break
|
||||||
|
case 'responseTimePercentage':
|
||||||
|
aValue = a.responseTimePercentage
|
||||||
|
bValue = b.responseTimePercentage
|
||||||
|
break
|
||||||
|
case 'averageResponseTime':
|
||||||
|
aValue = a.averageResponseTime
|
||||||
|
bValue = b.averageResponseTime
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
const comparison = aValue.localeCompare(bValue)
|
||||||
|
return sortDirection === 'asc' ? comparison : -comparison
|
||||||
|
} else {
|
||||||
|
const comparison = (aValue as number) - (bValue as number)
|
||||||
|
return sortDirection === 'asc' ? comparison : -comparison
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [baseResourceTypeBreakdown, sortColumn, sortDirection])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.breakdownSection}>
|
<div className={styles.breakdownSection}>
|
||||||
<h3>By Resource Type</h3>
|
<h3>By Resource Type</h3>
|
||||||
<div className={styles.breakdownTable}>
|
<div className={styles.breakdownTable}>
|
||||||
<BreakdownTableHeader categoryLabel="Resource Type" />
|
<BreakdownTableHeader
|
||||||
{resourceTypeBreakdown.map(item => (
|
categoryLabel="Resource Type"
|
||||||
|
sortColumn={sortColumn}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
/>
|
||||||
|
{sortedResourceTypeBreakdown.map(item => (
|
||||||
<div key={item.name} className={styles.tableRow}>
|
<div key={item.name} className={styles.tableRow}>
|
||||||
<span className={styles.categoryName}>{item.name}</span>
|
<span className={styles.categoryName}>{item.name}</span>
|
||||||
<span>{item.count}</span>
|
<span>{item.count}</span>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
||||||
import BreakdownTableHeader from './BreakdownTableHeader'
|
import BreakdownTableHeader, { type SortColumn, type SortDirection } from './BreakdownTableHeader'
|
||||||
import styles from './RequestBreakdown.module.css'
|
import styles from './RequestBreakdown.module.css'
|
||||||
|
|
||||||
interface CategoryBreakdown {
|
interface CategoryBreakdown {
|
||||||
@ -19,6 +19,8 @@ interface StatusCodeBreakdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ httpRequests }) => {
|
const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ httpRequests }) => {
|
||||||
|
const [sortColumn, setSortColumn] = useState<SortColumn | null>('count')
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
||||||
const formatSize = (bytes: number): string => {
|
const formatSize = (bytes: number): string => {
|
||||||
if (bytes < 1024) return `${bytes} B`
|
if (bytes < 1024) return `${bytes} B`
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
@ -31,7 +33,7 @@ const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ httpRequests
|
|||||||
return `${(microseconds / 1000000).toFixed(2)} s`
|
return `${(microseconds / 1000000).toFixed(2)} s`
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusCodeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
const baseStatusCodeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||||
const statusMap = new Map<string, HTTPRequest[]>()
|
const statusMap = new Map<string, HTTPRequest[]>()
|
||||||
|
|
||||||
httpRequests.forEach(req => {
|
httpRequests.forEach(req => {
|
||||||
@ -70,15 +72,83 @@ const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ httpRequests
|
|||||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||||
averageResponseTime: totalResponseTime / requests.length
|
averageResponseTime: totalResponseTime / requests.length
|
||||||
}
|
}
|
||||||
}).sort((a, b) => b.count - a.count)
|
})
|
||||||
}, [httpRequests])
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const handleSort = (column: SortColumn) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortColumn(column)
|
||||||
|
setSortDirection('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedStatusCodeBreakdown = useMemo(() => {
|
||||||
|
if (!sortColumn) return baseStatusCodeBreakdown
|
||||||
|
|
||||||
|
return [...baseStatusCodeBreakdown].sort((a, b) => {
|
||||||
|
let aValue: string | number
|
||||||
|
let bValue: string | number
|
||||||
|
|
||||||
|
switch (sortColumn) {
|
||||||
|
case 'name':
|
||||||
|
aValue = a.name
|
||||||
|
bValue = b.name
|
||||||
|
break
|
||||||
|
case 'count':
|
||||||
|
aValue = a.count
|
||||||
|
bValue = b.count
|
||||||
|
break
|
||||||
|
case 'percentage':
|
||||||
|
aValue = a.percentage
|
||||||
|
bValue = b.percentage
|
||||||
|
break
|
||||||
|
case 'totalSize':
|
||||||
|
aValue = a.totalSize
|
||||||
|
bValue = b.totalSize
|
||||||
|
break
|
||||||
|
case 'sizePercentage':
|
||||||
|
aValue = a.sizePercentage
|
||||||
|
bValue = b.sizePercentage
|
||||||
|
break
|
||||||
|
case 'totalResponseTime':
|
||||||
|
aValue = a.totalResponseTime
|
||||||
|
bValue = b.totalResponseTime
|
||||||
|
break
|
||||||
|
case 'responseTimePercentage':
|
||||||
|
aValue = a.responseTimePercentage
|
||||||
|
bValue = b.responseTimePercentage
|
||||||
|
break
|
||||||
|
case 'averageResponseTime':
|
||||||
|
aValue = a.averageResponseTime
|
||||||
|
bValue = b.averageResponseTime
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||||
|
const comparison = aValue.localeCompare(bValue)
|
||||||
|
return sortDirection === 'asc' ? comparison : -comparison
|
||||||
|
} else {
|
||||||
|
const comparison = (aValue as number) - (bValue as number)
|
||||||
|
return sortDirection === 'asc' ? comparison : -comparison
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [baseStatusCodeBreakdown, sortColumn, sortDirection])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.breakdownSection}>
|
<div className={styles.breakdownSection}>
|
||||||
<h3>By Status Code</h3>
|
<h3>By Status Code</h3>
|
||||||
<div className={styles.breakdownTable}>
|
<div className={styles.breakdownTable}>
|
||||||
<BreakdownTableHeader categoryLabel="Status Code" />
|
<BreakdownTableHeader
|
||||||
{statusCodeBreakdown.map(item => (
|
categoryLabel="Status Code"
|
||||||
|
sortColumn={sortColumn}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
/>
|
||||||
|
{sortedStatusCodeBreakdown.map(item => (
|
||||||
<div key={item.name} className={styles.tableRow}>
|
<div key={item.name} className={styles.tableRow}>
|
||||||
<span className={styles.categoryName}>{item.name}</span>
|
<span className={styles.categoryName}>{item.name}</span>
|
||||||
<span>{item.count}</span>
|
<span>{item.count}</span>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Chrome DevTools Performance Trace Format
|
* Chrome DevTools Performance Trace Format
|
||||||
* Based on the Trace Event Format specification
|
* Based on the Trace Event Format specification
|
||||||
|
Loading…
Reference in New Issue
Block a user