Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -15,57 +15,138 @@
|
||||
|
||||
$effect(() => {
|
||||
if (!open) return;
|
||||
const url = `/api/hosts/${encodeURIComponent(hostId)}/samples?` + new URLSearchParams({ scope, phase, page: String(page) });
|
||||
const url =
|
||||
`/api/hosts/${encodeURIComponent(hostId)}/samples?` +
|
||||
new URLSearchParams({ scope, phase, page: String(page) });
|
||||
void revision;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 15000);
|
||||
let active = true;
|
||||
loading = true; error = ''; result = null;
|
||||
loading = true;
|
||||
error = '';
|
||||
result = null;
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.message ?? body.error ?? 'Could not load samples.');
|
||||
if (!response.ok)
|
||||
throw new Error(body.message ?? body.error ?? 'Could not load samples.');
|
||||
if (active) result = body;
|
||||
} catch (cause) {
|
||||
if (active) error = controller.signal.aborted ? 'Sample request timed out. Try refreshing.' : (cause as Error).message;
|
||||
} finally { clearTimeout(timer); if (active) loading = false; }
|
||||
if (active)
|
||||
error = controller.signal.aborted
|
||||
? 'Sample request timed out. Try refreshing.'
|
||||
: (cause as Error).message;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
if (active) loading = false;
|
||||
}
|
||||
})();
|
||||
return () => { active = false; clearTimeout(timer); controller.abort(); };
|
||||
return () => {
|
||||
active = false;
|
||||
clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<details bind:open>
|
||||
<summary>View samples</summary>
|
||||
{#if open}
|
||||
<p>Retained minute peaks from the last 24 hours, used without extra allowance padding. CPU and memory peaks may come from different heartbeats. Choose the fleet to inspect measurements from other hosts used by fleet allowances.</p>
|
||||
<p>
|
||||
Retained minute peaks from the last 24 hours, used without extra allowance padding. CPU
|
||||
and memory peaks may come from different heartbeats. Choose the fleet to inspect
|
||||
measurements from other hosts used by fleet allowances.
|
||||
</p>
|
||||
<div class="controls">
|
||||
<label>Source<select bind:value={scope} onchange={() => page = 1}><option value="host">This host</option><option value="fleet">Entire fleet</option></select></label>
|
||||
<label>Phase<select bind:value={phase} onchange={() => page = 1}><option value="all">All phases</option><option value="startup">Starting</option><option value="running">Running</option></select></label>
|
||||
<Button size="sm" disabled={loading} onclick={() => { page = 1; revision++; }}>Refresh samples</Button>
|
||||
<label
|
||||
>Source<select bind:value={scope} onchange={() => (page = 1)}
|
||||
><option value="host">This host</option><option value="fleet"
|
||||
>Entire fleet</option
|
||||
></select
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Phase<select bind:value={phase} onchange={() => (page = 1)}
|
||||
><option value="all">All phases</option><option value="startup">Starting</option
|
||||
><option value="running">Running</option></select
|
||||
></label
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={loading}
|
||||
onclick={() => {
|
||||
page = 1;
|
||||
revision++;
|
||||
}}>Refresh samples</Button
|
||||
>
|
||||
</div>
|
||||
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
|
||||
{#if loading}<p role="status">Loading samples…</p>
|
||||
{:else if result}
|
||||
{#if result.total === 0}<p role="status">No {phase === 'all' ? '' : phase === 'startup' ? 'startup ' : 'running '}samples recorded for this source in the last 24 hours.</p>
|
||||
{#if result.total === 0}<p role="status">
|
||||
No {phase === 'all' ? '' : phase === 'startup' ? 'startup ' : 'running '}samples
|
||||
recorded for this source in the last 24 hours.
|
||||
</p>
|
||||
{:else}
|
||||
<p>{result.total.toLocaleString()} samples · newest first. Counts can differ from the allowance above, which is recalculated periodically.</p>
|
||||
<p>
|
||||
{result.total.toLocaleString()} samples · newest first. Counts can differ from the
|
||||
allowance above, which is recalculated periodically.
|
||||
</p>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (The overflow region needs keyboard focus for horizontal scrolling.) -->
|
||||
<div class="table-wrap" tabindex="0" role="region" aria-label="Container resource samples">
|
||||
<table><caption>CPU and memory peaks per container and minute; times are local.</caption>
|
||||
<thead><tr><th scope="col">Time</th>{#if scope === 'fleet'}<th scope="col">Host</th>{/if}<th scope="col">Server</th><th scope="col">Phase</th><th scope="col">CPU cores</th><th scope="col">Memory</th></tr></thead>
|
||||
<tbody>{#each result.samples as sample}
|
||||
<tr><td><RelativeTime value={sample.timestamp} /></td>
|
||||
{#if scope === 'fleet'}<td>{sample.hostName ?? sample.hostId}</td>{/if}
|
||||
<td>{sample.server ?? sample.instanceId}<small title={sample.instanceId}>Generation {sample.generation}</small></td>
|
||||
<td>{sample.phase === 'startup' ? 'Starting' : 'Running'}</td><td>{sample.cpuCores.toFixed(3)}</td><td>{(sample.memoryBytes / 1048576).toFixed(1)} MiB</td></tr>
|
||||
{/each}</tbody>
|
||||
<div
|
||||
class="table-wrap"
|
||||
tabindex="0"
|
||||
role="region"
|
||||
aria-label="Container resource samples"
|
||||
>
|
||||
<table>
|
||||
<caption
|
||||
>CPU and memory peaks per container and minute; times are local.</caption
|
||||
>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Time</th>{#if scope === 'fleet'}<th scope="col"
|
||||
>Host</th
|
||||
>{/if}<th scope="col">Server</th><th scope="col">Phase</th><th
|
||||
scope="col">CPU cores</th
|
||||
><th scope="col">Memory</th></tr
|
||||
></thead
|
||||
>
|
||||
<tbody
|
||||
>{#each result.samples as sample}
|
||||
<tr
|
||||
><td><RelativeTime value={sample.timestamp} /></td>
|
||||
{#if scope === 'fleet'}<td
|
||||
>{sample.hostName ?? sample.hostId}</td
|
||||
>{/if}
|
||||
<td
|
||||
>{sample.server ?? sample.instanceId}<small
|
||||
title={sample.instanceId}
|
||||
>Generation {sample.generation}</small
|
||||
></td
|
||||
>
|
||||
<td>{sample.phase === 'startup' ? 'Starting' : 'Running'}</td
|
||||
><td>{sample.cpuCores.toFixed(3)}</td><td
|
||||
>{(sample.memoryBytes / 1048576).toFixed(1)} MiB</td
|
||||
></tr
|
||||
>
|
||||
{/each}</tbody
|
||||
>
|
||||
</table>
|
||||
</div>
|
||||
<nav aria-label="Sample pages">
|
||||
<Button size="sm" disabled={result.page <= 1} onclick={() => page = result!.page - 1}>Previous</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={result.page <= 1}
|
||||
onclick={() => (page = result!.page - 1)}>Previous</Button
|
||||
>
|
||||
<span>Page {result.page} of {Math.ceil(result.total / result.pageSize)}</span>
|
||||
<Button size="sm" disabled={result.page * result.pageSize >= result.total} onclick={() => page = result!.page + 1}>Next</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={result.page * result.pageSize >= result.total}
|
||||
onclick={() => (page = result!.page + 1)}>Next</Button
|
||||
>
|
||||
</nav>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -73,13 +154,75 @@
|
||||
</details>
|
||||
|
||||
<style>
|
||||
details { margin-top:20px; min-width:0; } summary { cursor:pointer; color:var(--accent-primary); font-weight:600; }
|
||||
p, caption, small, nav { color:var(--text-secondary); font-size:var(--text-sm); line-height:1.6; }
|
||||
.controls, nav { display:flex; flex-wrap:wrap; align-items:end; gap:12px; margin:16px 0; }
|
||||
nav { align-items:center; } label { display:grid; gap:6px; font-size:var(--text-sm); }
|
||||
select { padding:8px; color:var(--text-primary); background:var(--surface-sunken); border:1px solid var(--border-control); border-radius:var(--radius-sm); }
|
||||
.table-wrap { max-width:100%; overflow:auto; } .table-wrap:focus-visible, summary:focus-visible { outline:2px solid var(--accent-primary); outline-offset:3px; }
|
||||
table { width:100%; border-collapse:collapse; text-align:left; font-size:var(--text-sm); }
|
||||
caption { text-align:left; padding-bottom:8px; } td,th { padding:10px; border-bottom:1px solid var(--border-subtle); }
|
||||
td { white-space:nowrap; } small { display:block; font-size:var(--text-xs); }
|
||||
details {
|
||||
margin-top: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
summary {
|
||||
cursor: pointer;
|
||||
color: var(--accent-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
p,
|
||||
caption,
|
||||
small,
|
||||
nav {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.controls,
|
||||
nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
nav {
|
||||
align-items: center;
|
||||
}
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
select {
|
||||
padding: 8px;
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-sunken);
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.table-wrap {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.table-wrap:focus-visible,
|
||||
summary:focus-visible {
|
||||
outline: 2px solid var(--accent-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
caption {
|
||||
text-align: left;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
small {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user