Files
electric-cherry/src/lib/ui/components/HostList.svelte
T

172 lines
5.8 KiB
Svelte

<script lang="ts">
import type { Host } from '$lib/hosts.js';
import { cpuTemperature, formatTemperature } from '$lib/host-temperatures.js';
import { contextHref } from '$lib/navigation-context.js';
import {
hostListStatus,
hostReadingsStale,
hostCpuUsage,
hostUsedSpace
} from '$ui/host-overview.js';
import { plural } from '$ui/plural.js';
import Badge from './primitives/Badge.svelte';
let {
hosts,
now,
returnTo = '/hosts'
}: {
hosts: (Host & { serverCount: number; playerCount: number; unreportedInstances: number })[];
now: number;
returnTo?: string;
} = $props();
</script>
<div class="table-wrap">
<table>
<thead
><tr
><th>Host</th><th>Status</th><th title="Assigned servers, including stopped servers"
>Servers</th
><th title="Players reported by online servers">Players</th><th>CPU</th><th
>Memory <span>used / total</span></th
><th>Disk <span>used / total</span></th></tr
></thead
>
<tbody
>{#each hosts as host (host.id)}
{@const status = hostListStatus(host, now)}
{@const stale = hostReadingsStale(host, now)}
<tr>
<td
><a class="name" href={contextHref(`/hosts/${host.id}`, returnTo)}
>{host.name}</a
>{#if host.hostname && host.hostname !== host.name}<span class="secondary"
>{host.hostname}</span
>{/if}
{#if stale && host.lastHeartbeat}<span class="secondary"
>Resource readings stale</span
>{/if}</td
>
<td class="status"
><Badge variant={status.tone}>{status.label}</Badge>{#if status.detail}<a
class="reason"
href={contextHref(`/hosts/${host.id}?tab=${status.tab}`, returnTo)}
>{status.detail}</a
>{/if}</td
>
<td
><a
href={contextHref(`/hosts/${host.id}?tab=servers`, returnTo)}
aria-label={`${host.serverCount} assigned ${plural(host.serverCount, 'server')} on ${host.name}`}
>{host.serverCount}</a
></td
>
<td
title={host.unreportedInstances
? `Player counts unavailable for ${host.unreportedInstances} of ${host.serverCount} ${plural(host.serverCount, 'server')}`
: 'Players reported by online servers'}
>{host.unreportedInstances === host.serverCount && host.serverCount > 0
? '—'
: `${host.playerCount}${host.unreportedInstances ? '+' : ''}`}</td
>
<td class:stale
>{hostCpuUsage(host.inventory.cpuUsagePercent)}<span class="secondary"
>CPU temp: {formatTemperature(
cpuTemperature(host.inventory.temperatures)?.celsius
)}</span
></td
>
<td class:stale
>{hostUsedSpace(
host.inventory.memoryTotal,
host.inventory.memoryAvailable
)}</td
>
<td class:stale
>{hostUsedSpace(
host.inventory.storageTotal,
host.inventory.storageAvailable
)}</td
>
</tr>
{/each}</tbody
>
</table>
</div>
<style>
.table-wrap {
overflow-x: auto;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
}
table {
width: 100%;
min-width: 850px;
border-collapse: collapse;
text-align: left;
font-size: var(--text-sm);
}
th {
padding: 12px 16px;
color: var(--text-secondary);
background: var(--surface-sunken);
font-weight: 500;
font-size: var(--text-xs);
}
th span {
display: block;
font-weight: 400;
margin-top: 3px;
}
td {
padding: 16px;
background: var(--surface);
border-top: 1px solid var(--border-subtle);
font-variant-numeric: tabular-nums;
}
tr:hover td {
background: var(--surface-raised);
}
a {
color: var(--accent-primary-hover);
}
.name {
color: var(--text-primary);
font-weight: 600;
text-decoration: none;
overflow-wrap: anywhere;
}
.name:hover {
color: var(--accent-primary-hover);
text-decoration: underline;
}
.status {
max-width: 330px;
min-width: 140px;
}
.secondary,
.reason {
display: block;
font-size: var(--text-xs);
color: var(--text-secondary);
margin-top: 6px;
line-height: 1.5;
}
.reason {
overflow-wrap: anywhere;
text-decoration: none;
}
.reason:hover {
text-decoration: underline;
color: var(--accent-primary-hover);
}
.stale {
color: var(--text-secondary);
}
a:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
</style>