coe pair filter

This commit is contained in:
2026-05-27 11:29:02 -04:00
parent 8da5957024
commit 5996443f38
8 changed files with 221 additions and 7 deletions
+1
View File
@@ -1,6 +1,7 @@
const FILTER_FIELDS = [
{ key: 'completed', label: 'Status', options: [{ value: '', label: 'All' }, { value: 'true', label: 'Completed' }, { value: 'false', label: 'Pending' }] },
{ key: 'interference', label: 'Type', options: [{ value: '', label: 'All' }, { value: 'COE', label: 'COE' }, { value: 'P2P', label: 'P2P' }, { value: 'P3P', label: 'P3P' }] },
{ key: 'coePair', label: 'COE Pairs', options: [{ value: '', label: 'All' }, { value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }] },
]
const DERIVED_FIELDS = [
+145 -3
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import StatusBadge from './StatusBadge'
const TYPE_COLORS = {
@@ -7,6 +7,35 @@ const TYPE_COLORS = {
P3P: 'bg-orange-900/50 text-orange-300',
}
function parsePairArray(raw) {
if (!raw) return []
if (Array.isArray(raw)) return raw
if (typeof raw !== 'string') return []
try {
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function parseTputRows(raw) {
if (!raw) return []
if (Array.isArray(raw)) return raw
if (typeof raw !== 'string') return []
try {
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
function testLookupKey(test) {
if (!test?.device || !test?.test_id) return null
return `${test.device}_${test.test_id}`
}
function fmtDuration(s) {
if (s == null) return null
const h = Math.floor(s / 3600)
@@ -41,9 +70,21 @@ function SortTh({ colKey, label, sort, onSort }) {
)
}
export default function TestTable({ tests = [], isLoading }) {
export default function TestTable({ tests = [], allTests = [], isLoading }) {
const [sort, setSort] = useState({ key: 'filename', dir: 1 })
const [expanded, setExpanded] = useState(new Set())
const [pairedExpanded, setPairedExpanded] = useState(new Set())
const sourceTests = allTests.length > 0 ? allTests : tests
const testsByLookupKey = useMemo(() => {
const map = new Map()
for (const t of sourceTests) {
const key = testLookupKey(t)
if (key) map.set(key, t)
}
return map
}, [sourceTests])
function toggleSort(key) {
setSort(s => ({ key, dir: s.key === key ? -s.dir : 1 }))
@@ -57,6 +98,15 @@ export default function TestTable({ tests = [], isLoading }) {
})
}
function togglePairedExpand(parentId, pairKey) {
const key = `${parentId}::${pairKey}`
setPairedExpanded(prev => {
const next = new Set(prev)
next.has(key) ? next.delete(key) : next.add(key)
return next
})
}
const sorted = [...tests].sort((a, b) => {
let av = a[sort.key] ?? ''
let bv = b[sort.key] ?? ''
@@ -142,6 +192,10 @@ export default function TestTable({ tests = [], isLoading }) {
<tr className={`border-t border-slate-700/60 ${test.completed ? 'bg-emerald-950/10' : 'bg-slate-900/80'}`}>
<td colSpan={3} className="px-6 py-4">
<div className="flex flex-wrap gap-3">
<TagPill label="COE Pairs" value={(() => {
const pairs = parsePairArray(test.coe_pair)
return pairs.length > 0 ? String(pairs.length) : null
})()} colorClass="bg-cyan-900/50 text-cyan-300" />
<TagPill label="File ID" value={test.test_id} />
<TagPill label="Type" value={test.interference} colorClass={TYPE_COLORS[test.interference]} />
<TagPill label="Device" value={test.device} />
@@ -157,7 +211,95 @@ export default function TestTable({ tests = [], isLoading }) {
<TagPill label="Elapsed Time" value={fmtDuration(test.duration_seconds)} />
</div>
{(() => {
const rows = test.tput_results ? JSON.parse(test.tput_results) : []
const pairs = parsePairArray(test.coe_pair)
if (pairs.length === 0) return null
return (
<div className="mt-3 pt-3 border-t border-slate-700/50">
<p className="text-[10px] uppercase tracking-wide text-slate-500 mb-2">Paired COE Tests</p>
<div className="flex flex-col gap-2">
{pairs.map(pairKey => {
const pairTest = testsByLookupKey.get(pairKey)
const itemKey = `${test.id}-${pairKey}`
const open = pairedExpanded.has(`${test.id}::${pairKey}`)
if (!pairTest) {
return (
<div key={itemKey} className="text-xs font-medium px-2.5 py-2 rounded bg-slate-800 text-slate-400">
{pairKey}
</div>
)
}
return (
<div key={itemKey} className="border border-slate-700/70 rounded-md overflow-hidden">
<button
type="button"
onClick={() => togglePairedExpand(test.id, pairKey)}
className="w-full px-2.5 py-2 bg-slate-900/70 hover:bg-slate-800 transition-colors flex items-center justify-between gap-2 text-left"
>
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs text-slate-500 select-none">{open ? '▾' : '▸'}</span>
<span className="text-xs text-slate-200 truncate">{pairTest.filename ?? pairKey}</span>
</div>
<StatusBadge completed={pairTest.completed} />
</button>
{open && (
<div className="px-3 py-3 border-t border-slate-700/60 bg-slate-900/40">
<div className="flex flex-wrap gap-3">
<TagPill label="File ID" value={pairTest.test_id} />
<TagPill label="Type" value={pairTest.interference} colorClass={TYPE_COLORS[pairTest.interference]} />
<TagPill label="Device" value={pairTest.device} />
<TagPill label="Rotation" value={pairTest.rotation} />
<TagPill label="Test Point" value={pairTest.test_point} />
<TagPill label="RSSI" value={pairTest.rssi} />
<TagPill label="Station" value={pairTest.station} />
<TagPill label="Band" value={pairTest.band} />
<TagPill label="Channel" value={pairTest.channel} />
<TagPill label="Bandwidth" value={pairTest.bandwidth} />
<TagPill label="Throttle" value={pairTest.throttled} />
<TagPill label="Direction" value={pairTest.direction} />
<TagPill label="Elapsed Time" value={fmtDuration(pairTest.duration_seconds)} />
</div>
{(() => {
const rows = parseTputRows(pairTest.tput_results)
if (!pairTest.completed || rows.length === 0) return null
return (
<div className="mt-3 pt-3 border-t border-slate-700/50">
<table className="text-xs w-auto border-collapse">
<thead>
<tr className="text-slate-500 uppercase tracking-wide">
<th className="pr-6 pb-1 text-left font-medium">Station</th>
<th className="pr-6 pb-1 text-right font-medium">Throughput</th>
<th className="pr-6 pb-1 text-right font-medium">DL RSSI</th>
<th className="pb-1 text-right font-medium">UL RSSI</th>
</tr>
</thead>
<tbody>
{rows.map(r => (
<tr key={`${pairTest.id}-${r.station}`} className="text-slate-300">
<td className="pr-6 py-0.5 text-cyan-400 font-medium">STA{r.station}</td>
<td className="pr-6 py-0.5 text-right">{r.tput} Mbps</td>
<td className="pr-6 py-0.5 text-right">{r.dlRssi} dBm</td>
<td className="py-0.5 text-right">{r.ulRssi} dBm</td>
</tr>
))}
</tbody>
</table>
</div>
)
})()}
</div>
)}
</div>
)
})}
</div>
</div>
)
})()}
{(() => {
const rows = parseTputRows(test.tput_results)
if (!test.completed || rows.length === 0) return null
return (
<div className="mt-3 pt-3 border-t border-slate-700/50">