diff options
| author | Adam Malczewski <[email protected]> | 2026-06-28 20:25:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-28 20:25:51 +0900 |
| commit | e5c01aa3640ac430155d96e9802dd523fe461319 (patch) | |
| tree | f6e669f4656fa97ece251f6ac6865290e66163c3 /src/features/concurrency/ui | |
| parent | 5bab0a158c13af474a4bd6ca3e1eefe8ce7a3924 (diff) | |
| download | dispatch-web-e5c01aa3640ac430155d96e9802dd523fe461319.tar.gz dispatch-web-e5c01aa3640ac430155d96e9802dd523fe461319.zip | |
fix(concurrency): Set+X on same line, status line with in-flight count and badge
Diffstat (limited to 'src/features/concurrency/ui')
| -rw-r--r-- | src/features/concurrency/ui/ConcurrencyLimitRow.svelte | 39 | ||||
| -rw-r--r-- | src/features/concurrency/ui/ConcurrencyView.svelte | 17 | ||||
| -rw-r--r-- | src/features/concurrency/ui/ConcurrencyView.test.ts | 29 |
3 files changed, 75 insertions, 10 deletions
diff --git a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte index 666b769..505847a 100644 --- a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte +++ b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte @@ -1,9 +1,13 @@ <script lang="ts"> import { untrack } from "svelte"; import { + DEFAULT_COOLDOWN_MS, parseCooldownInput, parseLimitInput, + statusLabel, + type Badge, type ConcurrencyLimitView, + type ConcurrencyStatusView, } from "../logic/view-model"; import type { DeleteConcurrencyLimit, @@ -13,20 +17,33 @@ let { limit, - cooldownMs, + status, save, saveCooldown, remove, }: { /** The configured limit row (providerId + current limit). */ limit: ConcurrencyLimitView; - /** The current per-slot release cooldown (ms) from the live status poll. */ - cooldownMs: number; + /** The provider's live status view (in-flight/queue/badge), or null when no + * status entry exists yet. Drives the status line + seeds the cooldown input. */ + status: ConcurrencyStatusView | null; save: SaveConcurrencyLimit; saveCooldown: SaveConcurrencyCooldown; remove: DeleteConcurrencyLimit; } = $props(); + // The badge→color map (presentational). Mirrors the old status-card mapping. + const badgeClass: Record<Badge, string> = { + success: "badge-success", + warning: "badge-warning", + error: "badge-error", + neutral: "badge-ghost", + }; + + // The cooldown input seed: the live cooldown when a status entry exists, else + // the server default (350). + const cooldownMs = $derived(status?.cooldownMs ?? DEFAULT_COOLDOWN_MS); + // Inline-edit state for the limit + cooldown inputs. Each is seeded from its // canonical value, but only while untouched — so a save echo / status-poll // refresh re-syncs without clobbering an in-flight edit. Mirrors the @@ -111,6 +128,7 @@ </script> <div class="flex flex-col gap-1 rounded-box bg-base-200 p-2 text-sm"> + <!-- Line 1: provider + limit + cooldown + Set + ✕ (all on one line). --> <div class="flex flex-wrap items-center gap-2"> <span class="min-w-0 flex-1 truncate font-medium font-mono" title={limit.providerId} >{limit.providerId}</span @@ -161,6 +179,21 @@ {/if} </button> </div> + + <!-- Line 2: in-flight count (left) + status badge (right). Hidden until the + first status poll for this provider lands. --> + {#if status !== null} + <div class="flex items-center justify-between gap-2 text-xs opacity-70"> + <span title="In-flight slots held vs cap">{status.inFlightLabel} in flight</span> + <span class="badge badge-sm {badgeClass[status.badge]} gap-1"> + {#if status.busy} + <span class="loading loading-spinner loading-xs"></span> + {/if} + {statusLabel(status)} + </span> + </div> + {/if} + {#if error} <span class="font-mono text-xs text-error">{error}</span> {:else if justSaved && !dirty} diff --git a/src/features/concurrency/ui/ConcurrencyView.svelte b/src/features/concurrency/ui/ConcurrencyView.svelte index 2f7435a..05d178a 100644 --- a/src/features/concurrency/ui/ConcurrencyView.svelte +++ b/src/features/concurrency/ui/ConcurrencyView.svelte @@ -9,6 +9,8 @@ providerOptions, summarizeLimits, viewConcurrencyLimits, + viewConcurrencyStatus, + type ConcurrencyStatusView, } from "../logic/view-model"; import type { ConcurrencyLimitEntry, @@ -201,12 +203,13 @@ * INVISIBLE, mirroring the heartbeat runs list). */ let statusInFlight = false; - // Per-provider cooldown lookup (from the live status poll) so each saved limit - // row seeds its cooldown input. Defaults to the server default (350) when a - // provider has no status entry yet. - const cooldownByProvider = $derived.by(() => { - const map = new Map<string, number>(); - for (const s of statusEntries) map.set(s.providerId, s.cooldownMs); + // Per-provider status view (from the live status poll) so each saved limit row + // renders its in-flight count + status badge + seeds its cooldown input. Null + // when a provider has no status entry yet (the row falls back to Idle + the + // server-default cooldown of 350). + const statusByProvider = $derived.by(() => { + const map = new Map<string, ConcurrencyStatusView>(); + for (const e of statusEntries) map.set(e.providerId, viewConcurrencyStatus(e)); return map; }); @@ -346,7 +349,7 @@ <li> <ConcurrencyLimitRow {limit} - cooldownMs={cooldownByProvider.get(limit.providerId) ?? DEFAULT_COOLDOWN_MS} + status={statusByProvider.get(limit.providerId) ?? null} save={rowSave} saveCooldown={cooldownSave} remove={rowRemove} diff --git a/src/features/concurrency/ui/ConcurrencyView.test.ts b/src/features/concurrency/ui/ConcurrencyView.test.ts index 68856a1..a8163c2 100644 --- a/src/features/concurrency/ui/ConcurrencyView.test.ts +++ b/src/features/concurrency/ui/ConcurrencyView.test.ts @@ -151,6 +151,35 @@ describe("ConcurrencyView", () => { expect((cooldownInput as HTMLInputElement).value).toBe("350"); }); + it("renders a status line (in-flight count left + badge right) below the edit line", async () => { + // Default status: limit 4, inFlight 2, queued 1, not paused → Active, "2/4". + const fakes = makeFakes(); + render(ConcurrencyView, { props: props(fakes) }); + + expect(await screen.findByText("2/4 in flight")).toBeVisible(); + expect(await screen.findByText("Active")).toBeVisible(); + }); + + it("shows the At-capacity badge when in-flight is at the cap with a queue", async () => { + const fakes = makeFakes({ + status: [statusEntry({ inFlight: 4, limit: 4, queued: 3 })], + }); + render(ConcurrencyView, { props: props(fakes) }); + + expect(await screen.findByText("4/4 in flight")).toBeVisible(); + expect(await screen.findByText("At capacity")).toBeVisible(); + }); + + it("shows the Idle badge when no slots are in flight", async () => { + const fakes = makeFakes({ + status: [statusEntry({ inFlight: 0, limit: 4, queued: 0 })], + }); + render(ConcurrencyView, { props: props(fakes) }); + + expect(await screen.findByText("0/4 in flight")).toBeVisible(); + expect(await screen.findByText("Idle")).toBeVisible(); + }); + it("surfaces NO loading indicator during refresh (background poll is silent — no flicker)", async () => { // The 2s status poll + post-mutation reloads are SILENT: they never toggle a // visible loading state, so the Refresh button is plain-text (no spinner). |
