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:
Michael Mainguy 2025-08-19 09:08:14 -05:00
parent 1035b28472
commit b5b7aafca5
6 changed files with 395 additions and 29 deletions

View File

@ -1,21 +1,121 @@
import React from 'react'
import styles from './RequestBreakdown.module.css'
export type SortColumn = 'name' | 'count' | 'percentage' | 'totalSize' | 'sizePercentage' | 'totalResponseTime' | 'responseTimePercentage' | 'averageResponseTime'
export type SortDirection = 'asc' | 'desc'
interface BreakdownTableHeaderProps {
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 (
<div className={styles.tableHeader}>
<span>{categoryLabel}</span>
<span>Request Count</span>
<span>Request Count %</span>
<span>Total Size</span>
<span>Total Size %</span>
<span>Total Response Time</span>
<span>Total Response Time %</span>
<span>Avg Response Time</span>
<button
className={styles.sortButton}
onClick={() => onSort('name')}
title={getTooltipText('name', categoryLabel)}
>
<span className={styles.sortButtonContent}>
{categoryLabel}
{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>
)
}

View File

@ -1,6 +1,6 @@
import React, { useMemo, useState } from 'react'
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'
interface CategoryBreakdown {
@ -20,6 +20,8 @@ interface HostnameBreakdownProps {
const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) => {
const [showAllHostnames, setShowAllHostnames] = useState(false)
const [sortColumn, setSortColumn] = useState<SortColumn | null>('count')
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
const formatSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`
@ -33,7 +35,7 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
return `${(microseconds / 1000000).toFixed(2)} s`
}
const hostnameBreakdown: CategoryBreakdown[] = useMemo(() => {
const baseHostnameBreakdown: CategoryBreakdown[] = useMemo(() => {
const hostMap = new Map<string, HTTPRequest[]>()
httpRequests.forEach(req => {
@ -72,15 +74,83 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
averageResponseTime: totalResponseTime / requests.length
}
}).sort((a, b) => b.count - a.count)
})
}, [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 (
<div className={styles.breakdownSection}>
<h3>By Hostname</h3>
<div className={styles.breakdownTable}>
<BreakdownTableHeader categoryLabel="Hostname" />
{(showAllHostnames ? hostnameBreakdown : hostnameBreakdown.slice(0, 10)).map(item => (
<BreakdownTableHeader
categoryLabel="Hostname"
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
/>
{(showAllHostnames ? sortedHostnameBreakdown : sortedHostnameBreakdown.slice(0, 10)).map(item => (
<div key={item.name} className={styles.tableRow}>
<span className={styles.categoryName}>{item.name}</span>
<span>{item.count}</span>
@ -117,11 +187,11 @@ const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) =
</div>
))}
</div>
{hostnameBreakdown.length > 10 && (
{sortedHostnameBreakdown.length > 10 && (
<div className={styles.moreInfo}>
{showAllHostnames ? (
<>
Showing all {hostnameBreakdown.length} hostnames
Showing all {sortedHostnameBreakdown.length} hostnames
<button
onClick={() => setShowAllHostnames(false)}
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
onClick={() => setShowAllHostnames(true)}
className={styles.toggleButton}

View File

@ -107,6 +107,61 @@
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 {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1.5fr 1fr 1.5fr 1fr 1.5fr;

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'
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'
interface CategoryBreakdown {
@ -19,6 +19,8 @@ interface ResourceTypeBreakdownProps {
}
const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ httpRequests }) => {
const [sortColumn, setSortColumn] = useState<SortColumn | null>('count')
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
const formatSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`
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`
}
const resourceTypeBreakdown: CategoryBreakdown[] = useMemo(() => {
const baseResourceTypeBreakdown: CategoryBreakdown[] = useMemo(() => {
const typeMap = new Map<string, HTTPRequest[]>()
httpRequests.forEach(req => {
@ -70,15 +72,83 @@ const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ httpReque
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
averageResponseTime: totalResponseTime / requests.length
}
}).sort((a, b) => b.count - a.count)
})
}, [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 (
<div className={styles.breakdownSection}>
<h3>By Resource Type</h3>
<div className={styles.breakdownTable}>
<BreakdownTableHeader categoryLabel="Resource Type" />
{resourceTypeBreakdown.map(item => (
<BreakdownTableHeader
categoryLabel="Resource Type"
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
/>
{sortedResourceTypeBreakdown.map(item => (
<div key={item.name} className={styles.tableRow}>
<span className={styles.categoryName}>{item.name}</span>
<span>{item.count}</span>

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'
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'
interface CategoryBreakdown {
@ -19,6 +19,8 @@ interface StatusCodeBreakdownProps {
}
const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ httpRequests }) => {
const [sortColumn, setSortColumn] = useState<SortColumn | null>('count')
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
const formatSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`
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`
}
const statusCodeBreakdown: CategoryBreakdown[] = useMemo(() => {
const baseStatusCodeBreakdown: CategoryBreakdown[] = useMemo(() => {
const statusMap = new Map<string, HTTPRequest[]>()
httpRequests.forEach(req => {
@ -70,15 +72,83 @@ const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ httpRequests
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
averageResponseTime: totalResponseTime / requests.length
}
}).sort((a, b) => b.count - a.count)
})
}, [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 (
<div className={styles.breakdownSection}>
<h3>By Status Code</h3>
<div className={styles.breakdownTable}>
<BreakdownTableHeader categoryLabel="Status Code" />
{statusCodeBreakdown.map(item => (
<BreakdownTableHeader
categoryLabel="Status Code"
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
/>
{sortedStatusCodeBreakdown.map(item => (
<div key={item.name} className={styles.tableRow}>
<span className={styles.categoryName}>{item.name}</span>
<span>{item.count}</span>

View File

@ -1,3 +1,4 @@
/**
* Chrome DevTools Performance Trace Format
* Based on the Trace Event Format specification