Skip to content

Commit ac581f2

Browse files
committed
feat: enchance ui
1 parent c1d4589 commit ac581f2

File tree

4 files changed

+219
-108
lines changed

4 files changed

+219
-108
lines changed

ui/html/src/connections.ejs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,26 @@
2222
</div>
2323
</header>
2424
<main class="flex-1 p-0">
25-
<ul id="connection-list" class="divide-y divide-neutral-200 dark:divide-neutral-800"></ul>
25+
<div class="overflow-x-auto">
26+
<table class="min-w-full text-sm text-left text-neutral-800 dark:text-neutral-100">
27+
<thead class="bg-neutral-50 dark:bg-neutral-800/50 text-neutral-700 dark:text-neutral-300">
28+
<tr>
29+
<th class="px-3 py-2 cursor-pointer" data-key="id">ID</th>
30+
<th class="px-3 py-2 cursor-pointer" data-key="src">Source</th>
31+
<th class="px-3 py-2 cursor-pointer" data-key="path">Path</th>
32+
<th class="px-3 py-2 cursor-pointer" data-key="protocols">Protocols</th>
33+
<th class="px-3 py-2 cursor-pointer" data-key="bytesrx">BytesRx</th>
34+
<th class="px-3 py-2 cursor-pointer" data-key="bytestx">BytesTx</th>
35+
<th class="px-3 py-2">Start Time</th>
36+
<th class="px-3 py-2 cursor-pointer" data-key="starttime">Elapsed</th>
37+
<th class="px-3 py-2">SpeedRx</th>
38+
<th class="px-3 py-2">SpeedTx</th>
39+
<th class="px-3 py-2">Actions</th>
40+
</tr>
41+
</thead>
42+
<tbody id="connection-tbody" class="divide-y divide-neutral-200 dark:divide-neutral-800"></tbody>
43+
</table>
44+
</div>
2645
</main>
2746
</div>
2847

ui/html/src/connections.ts

Lines changed: 91 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ type Connection = {
99
starttime: string
1010
}
1111

12+
type SortKey = 'id' | 'src' | 'path' | 'protocols' | 'bytesrx' | 'bytestx' | 'starttime'
13+
1214
const previousData: Record<string, { bytesRx: number; bytesTx: number; timestamp: number }> = {}
15+
const tbody = document.getElementById('connection-tbody') as HTMLTableSectionElement
16+
let currentSort: { key: SortKey, dir: 'asc' | 'desc' } = { key: 'starttime', dir: 'desc' }
1317

1418
function formatSpeed(speed: number): string {
1519
if (!isFinite(speed)) return '0 B/s'
@@ -55,82 +59,107 @@ async function killConnection(cid: string) {
5559
}
5660
}
5761

58-
async function fetchData() {
62+
async function updateUptime() {
5963
try {
60-
const resp = await fetch('/api/v1/tcp/connections')
61-
const json = await resp.json() as Record<string, Connection>
62-
displayData(json)
63-
} catch (e) {
64-
console.error('Error:', e)
64+
const resp = await fetch('/api/v1/uptime', { redirect: 'error' })
65+
if (!resp.ok) throw new Error('bad')
66+
const text = await resp.text()
67+
const el = document.getElementById('uptime')
68+
if (el) { el.textContent = text; el.classList.remove('disconnected') }
69+
} catch {
70+
const el = document.getElementById('uptime')
71+
if (el) { el.textContent = 'Disconnected'; el.classList.add('disconnected') }
6572
}
6673
}
6774

68-
function displayData(connections: Record<string, Connection>) {
69-
const ul = document.getElementById('connection-list') as HTMLUListElement
70-
ul.innerHTML = ''
75+
function compare(a: any, b: any, key: SortKey): number {
76+
if (key === 'bytesrx' || key === 'bytestx') {
77+
return Number(a[key] ?? 0) - Number(b[key] ?? 0)
78+
}
79+
if (key === 'starttime') {
80+
return new Date(a[key]).getTime() - new Date(b[key]).getTime()
81+
}
82+
return String(a[key] ?? '').localeCompare(String(b[key] ?? ''))
83+
}
84+
85+
function renderRows(rows: Array<Record<string, any>>) {
86+
tbody.innerHTML = ''
7187
const now = Date.now()
72-
Object.keys(connections).forEach(key => {
73-
const c = connections[key]
74-
const li = document.createElement('li')
75-
li.className = 'py-3'
76-
77-
const title = document.createElement('div')
78-
title.className = 'text-sm font-medium text-neutral-900 dark:text-neutral-100'
79-
title.textContent = `Connection ID: ${key}`
80-
li.appendChild(title)
81-
82-
const start = new Date(c.starttime)
83-
const prev = previousData[key] ?? { bytesRx: c.bytesrx, bytesTx: c.bytestx, timestamp: now }
88+
for (const row of rows) {
89+
const tr = document.createElement('tr')
90+
tr.className = 'hover:bg-neutral-50 dark:hover:bg-neutral-800/40'
91+
92+
const start = new Date(row.starttime)
93+
const prev = previousData[row.id] ?? { bytesRx: row.bytesrx, bytesTx: row.bytestx, timestamp: now }
8494
const dt = Math.max(0.001, (now - prev.timestamp) / 1000)
85-
const speedRx = (c.bytesrx - prev.bytesRx) / dt
86-
const speedTx = (c.bytestx - prev.bytesTx) / dt
87-
88-
const meta = document.createElement('div')
89-
meta.className = 'mt-2 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-1 text-xs text-neutral-700 dark:text-neutral-300 font-mono'
90-
meta.innerHTML = `
91-
<div>BytesRx: ${c.bytesrx}</div>
92-
<div>BytesTx: ${c.bytestx}</div>
93-
<div>Path: ${c.path}</div>
94-
<div>Protocols: ${c.protocols}</div>
95-
<div>Source: ${c.src}</div>
96-
<div>Start: ${start.toLocaleString()}</div>
97-
<div>SpeedRx: ${formatSpeed(speedRx)}</div>
98-
<div>SpeedTx: ${formatSpeed(speedTx)}</div>
99-
<div>Elapsed: ${formatTime(now - start.getTime())}</div>
95+
const speedRx = (row.bytesrx - prev.bytesRx) / dt
96+
const speedTx = (row.bytestx - prev.bytesTx) / dt
97+
98+
tr.innerHTML = `
99+
<td class="px-3 py-2 font-mono">${row.id}</td>
100+
<td class="px-3 py-2 font-mono text-xs">${row.src}</td>
101+
<td class="px-3 py-2">${row.path}</td>
102+
<td class="px-3 py-2">${row.protocols}</td>
103+
<td class="px-3 py-2">${row.bytesrx}</td>
104+
<td class="px-3 py-2">${row.bytestx}</td>
105+
<td class="px-3 py-2 font-mono text-xs">${row.starttime}</td>
106+
<td class="px-3 py-2 font-mono">${formatTime(now - start.getTime())}</td>
107+
<td class="px-3 py-2 font-mono text-xs">${formatSpeed(speedRx)}</td>
108+
<td class="px-3 py-2 font-mono text-xs">${formatSpeed(speedTx)}</td>
109+
<td class="px-3 py-2">
110+
<button data-cid="${row.id}" class="px-2 py-1 rounded-md bg-red-600 text-white hover:bg-red-700 text-xs">Kill</button>
111+
</td>
100112
`
101-
li.appendChild(meta)
113+
tbody.appendChild(tr)
102114

103-
previousData[key] = { bytesRx: c.bytesrx, bytesTx: c.bytestx, timestamp: now }
115+
previousData[row.id] = { bytesRx: row.bytesrx, bytesTx: row.bytestx, timestamp: now }
116+
}
117+
}
104118

105-
const actions = document.createElement('div')
106-
actions.className = 'mt-2'
107-
const killBtn = document.createElement('button')
108-
killBtn.className = 'px-3 py-1.5 rounded-md bg-red-600 text-white hover:bg-red-700'
109-
killBtn.textContent = 'Kill'
110-
killBtn.addEventListener('click', () => killConnection(key))
111-
actions.appendChild(killBtn)
112-
li.appendChild(actions)
119+
async function fetchData() {
120+
try {
121+
const resp = await fetch('/api/v1/tcp/connections')
122+
const json = await resp.json() as Record<string, Connection>
123+
const rows = Object.keys(json).map(id => ({ id, ...(json[id] || {}) }))
124+
rows.sort((a, b) => {
125+
const cmp = compare(a, b, currentSort.key)
126+
return currentSort.dir === 'asc' ? cmp : -cmp
127+
})
128+
renderRows(rows)
129+
} catch (e) {
130+
console.error('Error:', e)
131+
}
132+
}
113133

114-
ul.appendChild(li)
134+
function setupSorting() {
135+
const heads = document.querySelectorAll('thead th[data-key]')
136+
heads.forEach(th => {
137+
th.addEventListener('click', () => {
138+
const key = (th as HTMLElement).dataset.key as SortKey
139+
if (!key) return
140+
if (currentSort.key === key) {
141+
currentSort = { key, dir: currentSort.dir === 'asc' ? 'desc' : 'asc' }
142+
} else {
143+
currentSort = { key, dir: 'asc' }
144+
}
145+
void fetchData()
146+
})
115147
})
116148
}
117149

118-
async function updateUptime() {
119-
try {
120-
const resp = await fetch('/api/v1/uptime', { redirect: 'error' })
121-
if (!resp.ok) throw new Error('bad')
122-
const text = await resp.text()
123-
const el = document.getElementById('uptime')
124-
if (el) { el.textContent = text; el.classList.remove('disconnected') }
125-
} catch {
126-
const el = document.getElementById('uptime')
127-
if (el) { el.textContent = 'Disconnected'; el.classList.add('disconnected') }
128-
}
150+
function setupActions() {
151+
tbody.addEventListener('click', (ev) => {
152+
const target = ev.target as HTMLElement
153+
const btn = target.closest('button[data-cid]') as HTMLButtonElement | null
154+
if (btn && btn.dataset.cid) {
155+
void killConnection(btn.dataset.cid)
156+
}
157+
})
129158
}
130159

160+
setupSorting()
161+
setupActions()
131162
setInterval(fetchData, 1000)
132-
fetchData()
133-
updateUptime()
163+
void fetchData()
164+
void updateUptime()
134165
setInterval(updateUptime, 1000)
135-
136-

ui/html/src/requests.ejs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,27 @@
2222
</div>
2323
</header>
2424
<main class="flex-1 p-0">
25-
<ul class="divide-y divide-neutral-200 dark:divide-neutral-800" id="request-list"></ul>
25+
<div class="overflow-x-auto">
26+
<table class="min-w-full text-sm text-left text-neutral-800 dark:text-neutral-100">
27+
<thead class="bg-neutral-50 dark:bg-neutral-800/50 text-neutral-700 dark:text-neutral-300">
28+
<tr>
29+
<th class="px-3 py-2 cursor-pointer" data-key="id">ID</th>
30+
<th class="px-3 py-2 cursor-pointer" data-key="method">Method</th>
31+
<th class="px-3 py-2 cursor-pointer" data-key="host">Host</th>
32+
<th class="px-3 py-2 cursor-pointer" data-key="uri">URI</th>
33+
<th class="px-3 py-2 cursor-pointer" data-key="src">Src</th>
34+
<th class="px-3 py-2 cursor-pointer" data-key="protocol">Proto</th>
35+
<th class="px-3 py-2 cursor-pointer" data-key="code">Code</th>
36+
<th class="px-3 py-2 cursor-pointer" data-key="enc">Enc</th>
37+
<th class="px-3 py-2 cursor-pointer" data-key="respwritten">Tx</th>
38+
<th class="px-3 py-2">Start Time</th>
39+
<th class="px-3 py-2 cursor-pointer" data-key="starttime">Elapsed</th>
40+
<th class="px-3 py-2">Actions</th>
41+
</tr>
42+
</thead>
43+
<tbody id="request-tbody" class="divide-y divide-neutral-200 dark:divide-neutral-800"></tbody>
44+
</table>
45+
</div>
2646
</main>
2747
</div>
2848

0 commit comments

Comments
 (0)