134 lines
3.2 KiB
Svelte
134 lines
3.2 KiB
Svelte
<script lang="ts">
|
|
import StateDot, { type DotTone } from "./StateDot.svelte";
|
|
import { COMPONENT_DETAIL, COMPONENT_LABEL, STACK_STATE_LABEL } from "../lib/format";
|
|
import { STACK_ORDER, type StackMap } from "../lib/launcher.svelte";
|
|
import type { Phase, StackComponent, StackState } from "../lib/types";
|
|
|
|
interface Props {
|
|
stack: StackMap;
|
|
phase: Phase;
|
|
}
|
|
|
|
let { stack, phase }: Props = $props();
|
|
|
|
type Shown = StackState | "waiting";
|
|
|
|
const TONE: Record<Shown, DotTone> = {
|
|
waiting: "idle",
|
|
starting: "warn",
|
|
ready: "ok",
|
|
stopped: "idle",
|
|
failed: "danger",
|
|
};
|
|
|
|
// Dev runs add the web client, and a web-only run has no ODIAN row to show at all.
|
|
const shown = $derived.by(() => {
|
|
const rows = STACK_ORDER.filter((c) => c !== "odian" || stack.odian || !stack.web);
|
|
return stack.web ? [...rows, "web" as StackComponent] : rows;
|
|
});
|
|
|
|
function shownState(component: StackComponent): Shown {
|
|
const event = stack[component];
|
|
if (event) return event.state;
|
|
// No event seen yet (e.g. the UI reloaded while ODIAN was already open).
|
|
return phase === "running" ? "ready" : "waiting";
|
|
}
|
|
</script>
|
|
|
|
<ol class="stack" aria-label="Local stack">
|
|
{#each shown as component, index (component)}
|
|
{@const state = shownState(component)}
|
|
{@const message = stack[component]?.message}
|
|
<li class="row {state}">
|
|
<span class="index mono" aria-hidden="true">{String(index + 1).padStart(2, "0")}</span>
|
|
<span class="dot-cell"><StateDot tone={TONE[state]} pulse={state === "starting"} /></span>
|
|
<span class="name">
|
|
{COMPONENT_LABEL[component]}
|
|
<span class="detail mono">{COMPONENT_DETAIL[component]}</span>
|
|
</span>
|
|
<span class="state mono">{STACK_STATE_LABEL[state]}</span>
|
|
{#if message}
|
|
<span class="message mono selectable" title={message}>{message}</span>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ol>
|
|
|
|
<style>
|
|
.stack {
|
|
display: grid;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
|
|
.row {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 22px 20px minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
column-gap: 8px;
|
|
min-height: 44px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.index {
|
|
color: var(--muted);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.dot-cell {
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.name {
|
|
overflow: hidden;
|
|
color: var(--ink-2);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.detail {
|
|
margin-left: 6px;
|
|
color: var(--muted);
|
|
font-size: var(--text-xs);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.state {
|
|
color: var(--muted);
|
|
font-size: var(--text-xs);
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.message {
|
|
grid-column: 3 / 5;
|
|
overflow: hidden;
|
|
color: var(--muted);
|
|
font-size: var(--text-xs);
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.ready .state {
|
|
color: var(--ok);
|
|
}
|
|
.starting .state {
|
|
color: var(--warn);
|
|
}
|
|
.failed .state,
|
|
.failed .message {
|
|
color: var(--danger);
|
|
}
|
|
.ready .name,
|
|
.starting .name {
|
|
color: var(--ink);
|
|
}
|
|
</style>
|