Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d2d6f86
feat(comhub): wire up getMetadata, reactive sync, disconnect stubs, u…
anmol-virk Mar 9, 2026
483985d
fix(comhub): improve light/dark mode styling for endpoint and interfa…
anmol-virk Mar 9, 2026
edb277b
feat(comhub): wire up live runtime metadata, remove mock data, implem…
anmol-virk Mar 11, 2026
e7884a8
feat(runtime): connect to example.unyt.land websocket server for dev …
anmol-virk Mar 12, 2026
61e1ddb
refactor: replace useComHub composable with Datex.endpoint directly
anmol-virk Mar 12, 2026
11252c4
fix(comhub): convert known_since from milliseconds to seconds in form…
anmol-virk Mar 12, 2026
bf3f343
fix(comhub): fix heading visibility in light mode
anmol-virk Mar 12, 2026
2c4e60d
fix(comhub): set max-width on search bar instead of w-1/2
anmol-virk Mar 12, 2026
3262b90
refactor(comhub): replace tabs with toggle to switch between interfac…
anmol-virk Mar 12, 2026
1eee38f
refactor(comhub): split ComHub into shared wrapper with InterfaceList…
anmol-virk Mar 12, 2026
3e9c6c7
fix(comhub): remove console.warn from disconnectInterface
anmol-virk Mar 12, 2026
1467198
fix(runtime): restore original getPointers mock with DIFValueContaine…
anmol-virk Mar 12, 2026
6337f55
fix(runtime): remove try/catch from getComHubMetadata, propagate errors
anmol-virk Mar 12, 2026
e725130
fix(runtime): clean up runtime.ts after merge conflict
anmol-virk Mar 12, 2026
32bd709
fix(comhub): calculate relative time from unix timestamp using Date.n…
anmol-virk Mar 13, 2026
a7b4ec7
fix(comhub): simplify disconnectSocket function and remove unused button
jonasstrehle Mar 13, 2026
982e0f6
fix(comhub): replace disconnectSocket with removeSocket and update di…
jonasstrehle Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"@unyt/datex": "^0.0.10",
"@unyt/datex": "0.0.13",
"@unyt/speck": "^0.0.11",
"@vueuse/core": "^14.0.0",
"class-variance-authority": "^0.7.1",
Expand Down
171 changes: 171 additions & 0 deletions src/components/ComHubEndpointList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<template>
<div>
<div
v-for="[endpointId, sockets] in filteredEndpoints"
:key="endpointId"
class="border border-neutral-200 dark:border-neutral-700 rounded-lg p-3 mb-3 bg-white dark:bg-neutral-900 shadow-sm text-neutral-900 dark:text-neutral-100"
>
<!-- Endpoint Header -->
<div class="flex justify-between items-center cursor-pointer" @click="toggle(endpointId)">
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<h3 class="font-semibold text-sm font-mono text-blue-600 dark:text-blue-400">{{ endpointId }}</h3>
<span
v-if="isDirect(sockets)"
class="text-xs px-2 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"
>direct</span>
</div>
<div class="text-xs text-neutral-500">
Connected via {{ sockets.length }} {{ sockets.length === 1 ? 'socket' : 'sockets' }}
{{ getEndpointDirection(sockets) }}
</div>
</div>
<div class="flex items-center gap-2">
<button
v-if="advancedMode"
@click.stop="sockets[0] && disconnectInterface(sockets[0].interface.uuid)"
class="text-xs px-2 py-1 rounded bg-red-100 text-red-600 hover:bg-red-200 dark:bg-red-900 dark:text-red-300 dark:hover:bg-red-800 transition"
>
Disconnect
</button>
<span class="text-neutral-400 text-xs">{{ expanded[endpointId] ? '▾' : '▸' }}</span>
</div>
</div>

<!-- Expanded Socket List -->
<div v-if="expanded[endpointId]" class="mt-3 border-t pt-3 flex flex-col gap-2">
<div
v-for="(socket, idx) in sockets"
:key="socket.uuid + idx"
class="text-sm p-3 rounded bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700"
>
<div class="flex items-start justify-between gap-2">
<div class="flex flex-col gap-1 flex-1">
<h4 class="font-semibold text-sm">
{{ socket.interface.properties.interface_type }}
<span v-if="socket.interface.properties.name" class="font-normal text-neutral-500">
({{ socket.interface.properties.name }})
</span>
</h4>
<div class="text-xs text-neutral-400 font-mono mt-0.5">{{ socket.uuid }}</div>
<div class="text-xs text-neutral-500 mt-1 flex items-center gap-2">
<span>Known since {{ formatTime(socket.properties.known_since) }}</span>
<span>·</span>
<span>Distance: {{ socket.properties.distance }}</span>
<span
v-if="socket.properties.is_direct"
class="text-xs px-2 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"
>direct</span>
</div>
</div>
<button
v-if="advancedMode"
@click.stop="disconnectSocket(socket.interface.uuid, socket.uuid, endpointId)"
class="shrink-0 text-xs px-2 py-1 rounded bg-red-100 text-red-600 hover:bg-red-200 dark:bg-red-900 dark:text-red-300 dark:hover:bg-red-800 transition"
>
Disconnect
</button>
</div>
</div>
</div>
</div>

<div v-if="filteredEndpoints.length === 0" class="text-sm text-neutral-500">
No matching endpoints found.
</div>
</div>
</template>

<script setup lang="ts">
import { reactive, computed, watch } from 'vue'
import { Datex } from '@/lib/runtime'
import type { ComHubInterface, ComHubSocket } from '@/components/ComHubOverviewWrapper.vue'

interface SocketWithInterface extends ComHubSocket {
interface: {
uuid: string
properties: ComHubInterface['properties']
}
}

const props = defineProps<{
interfaces: ComHubInterface[]
searchQuery: string
advancedMode: boolean
}>()

const expanded = reactive<Record<string, boolean>>({})

const endpointMap = computed((): Map<string, SocketWithInterface[]> => {
const map = new Map<string, SocketWithInterface[]>()
for (const iface of props.interfaces) {
for (const socket of iface.sockets) {
const entry: SocketWithInterface = {
...socket,
interface: { uuid: iface.uuid, properties: iface.properties },
}
if (!map.has(socket.endpoint)) map.set(socket.endpoint, [])
map.get(socket.endpoint)!.push(entry)
}
}
return map
})

const filteredEndpoints = computed((): [string, SocketWithInterface[]][] => {
const query = props.searchQuery.trim().toLowerCase()
const entries = Array.from(endpointMap.value.entries())
if (!query) return entries
return entries.filter(([endpointId]) => endpointId.toLowerCase().includes(query))
})

watch(filteredEndpoints, (entries) => {
if (!props.searchQuery.trim()) return
for (const [endpointId] of entries) {
expanded[endpointId] = true
}
})

watch(() => props.searchQuery, (val) => {
if (!val.trim()) {
Object.keys(expanded).forEach((key) => { expanded[key] = false })
}
})

function toggle(endpointId: string) {
expanded[endpointId] = !expanded[endpointId]
}

function getEndpointDirection(sockets: SocketWithInterface[]): string {
const dirs = new Set(sockets.map((s) => s.direction))
if (dirs.has('InOut')) return '↔'
if (dirs.has('In') && dirs.has('Out')) return '↔'
if (dirs.has('In')) return '←'
if (dirs.has('Out')) return '→'
return '↔'
}

function isDirect(sockets: SocketWithInterface[]): boolean {
return sockets.some((s) => s.properties.is_direct)
}

const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })

function formatTime(ms: number): string {
const elapsed = Math.floor((Date.now() - ms) / 1000)
if (elapsed < 60) return rtf.format(-elapsed, 'second')
const minutes = Math.floor(elapsed / 60)
if (minutes < 60) return rtf.format(-minutes, 'minute')
const hours = Math.floor(minutes / 60)
if (hours < 24) return rtf.format(-hours, 'hour')
return rtf.format(-Math.floor(hours / 24), 'day')
}

async function disconnectSocket(interfaceUuid: string, socketUuid: string, endpoint: string) {
await Datex.comHub.removeSocket(socketUuid as `socket::${string}`)
console.warn('[ComHub] disconnectSocket called', { interfaceUuid, socketUuid, endpoint })
}

async function disconnectInterface(interfaceUuid: string) {
await Datex.comHub.removeInterface(interfaceUuid as `com_interface::${string}`)
}
</script>
Loading
Loading