File
Blob: ui/operator/Clients.svelte
| 1 | <script lang="ts"> |
| 2 | import Table from './Table.svelte'; |
| 3 | import SnapshotTime from './SnapshotTime.svelte'; |
| 4 | import type { ClientList } from './types'; |
| 5 | |
| 6 | let { data }: { data: ClientList } = $props(); |
| 7 | let expanded = $state<Record<string, boolean>>({}); |
| 8 | </script> |
| 9 | |
| 10 | <p class="text-muted wrap-anywhere">Node <span class="ml-1 font-mono text-[0.92em] text-foreground">{data.node}</span></p> |
| 11 | <div class="mt-6 mb-2.5 flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1"> |
| 12 | <h2 class="text-[15px] font-semibold">{data.clients.length} connected {data.clients.length === 1 ? 'client' : 'clients'}</h2> |
| 13 | <p class="text-[13px] text-muted">Observed <SnapshotTime value={data.observedAt} /></p> |
| 14 | </div> |
| 15 | {#if data.clients.length} |
| 16 | <Table label="Connected clients" tableClass="table-fixed min-[600px]:min-w-[42rem]"> |
| 17 | <thead><tr><th scope="col" class="w-full min-[600px]:w-[74%]">Hostname / client</th><th scope="col" class="hidden min-[600px]:table-cell min-[600px]:w-[18%]">Peer address</th><th scope="col" class="hidden min-[600px]:table-cell min-[600px]:w-[8%]">Version</th></tr></thead> |
| 18 | <tbody> |
| 19 | {#each data.clients as client, index (`${client.identity}/${client.address}`)} |
| 20 | {@const key = `${client.identity}/${client.address}`} |
| 21 | {@const clientID = client.clientId} |
| 22 | <tr> |
| 23 | <th scope="row"> |
| 24 | <div class="flex items-baseline gap-2"> |
| 25 | {#if client.sessionMode} |
| 26 | <span class={['min-w-0 font-mono text-[0.92em]', client.sessionMode === 'ephemeral' && 'break-all']}>{client.hostname || `Client ${clientID}`}</span> |
| 27 | {:else} |
| 28 | <a class="-my-2 inline-flex min-h-10 min-w-0 items-center font-mono text-[0.92em] text-accent-300 underline underline-offset-4 hover:text-accent-100" href={client.url}>Client {clientID}</a> |
| 29 | {/if} |
| 30 | <span class="shrink-0 rounded-full border border-line bg-chrome px-2 py-0.5 text-xs leading-4 text-muted"><span class="sr-only">Type: </span>{client.sessionMode === 'ephemeral' ? 'Ephemeral' : client.sessionMode === 'token' ? 'Token' : 'Client'}</span> |
| 31 | </div> |
| 32 | <div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-muted"> |
| 33 | {#if client.sessionMode === 'token' && client.ownerIdentity} |
| 34 | <span>Owner {#if client.ownerUrl}<a class="text-muted underline underline-offset-4 hover:text-foreground" href={client.ownerUrl}>{client.ownerLabel || client.ownerIdentity}</a>{:else}{client.ownerLabel || client.ownerIdentity}{/if}</span> |
| 35 | <span aria-hidden="true">·</span> |
| 36 | {/if} |
| 37 | <button |
| 38 | type="button" |
| 39 | class="-my-1.5 inline-flex min-h-9 shrink-0 items-center gap-1 whitespace-nowrap text-muted hover:text-foreground" |
| 40 | aria-expanded={Boolean(expanded[key])} |
| 41 | aria-controls={`client-identity-${index}`} |
| 42 | onclick={() => { expanded[key] = !expanded[key]; }} |
| 43 | ><span class="inline-block w-3 text-center" aria-hidden="true">{expanded[key] ? '▾' : '▸'}</span>Identity</button> |
| 44 | </div> |
| 45 | <div id={`client-identity-${index}`} hidden={!expanded[key]} class="mt-1 text-[13px] leading-relaxed text-muted"> |
| 46 | <p><span class="mr-2">Connection identity</span><code class="font-mono text-[0.92em] break-all text-foreground">{client.identity}</code></p> |
| 47 | {#if client.ownerIdentity}<p class="mt-1"><span class="mr-2">Owner identity</span><code class="font-mono text-[0.92em] break-all text-foreground">{client.ownerIdentity}</code></p>{/if} |
| 48 | </div> |
| 49 | <div class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted min-[600px]:hidden"> |
| 50 | <span>Peer address <span class="font-mono text-foreground">{client.address}</span></span> |
| 51 | <span>Version <span class="font-mono text-foreground">{client.version || 'Not reported'}</span></span> |
| 52 | </div> |
| 53 | </th> |
| 54 | <td class="hidden font-mono text-[0.92em] min-[600px]:table-cell">{client.address}</td> |
| 55 | <td class="hidden font-mono text-[0.92em] min-[600px]:table-cell"> |
| 56 | {#if client.version}{client.version}{:else}<span class="font-sans text-muted">Not reported</span>{/if} |
| 57 | </td> |
| 58 | </tr> |
| 59 | {/each} |
| 60 | </tbody> |
| 61 | </Table> |
| 62 | {:else} |
| 63 | <p class="border-y border-line py-6 text-muted">No clients connected to this node.</p> |
| 64 | {/if} |