ui: move workers list to page
This commit is contained in:
@@ -349,6 +349,17 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-heading-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.section-heading-link:hover,
|
||||
.section-heading-link:focus-visible,
|
||||
.section-heading-link.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--accent-muted);
|
||||
margin: 0;
|
||||
@@ -635,6 +646,44 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workers-page {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.workers-page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.workers-page-header h1,
|
||||
.workers-page-header p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.workers-page-header h1 {
|
||||
color: var(--text-strong);
|
||||
font-size: clamp(1.6rem, 4vw, 2.4rem);
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.workers-page-header p {
|
||||
margin-top: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.workers-table-wrap {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.workers-table td small {
|
||||
display: block;
|
||||
margin-top: 0.2rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.diagnostics {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("workspace Worker list and sidebar attach through Worker Console hrefs", async () => {
|
||||
Deno.test("workspace Worker list lives on the dedicated Workers page", async () => {
|
||||
const workspacePage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/+page.svelte", import.meta.url),
|
||||
);
|
||||
const workersPage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/workers/+page.svelte", import.meta.url),
|
||||
);
|
||||
const workersNav = await Deno.readTextFile(
|
||||
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url),
|
||||
);
|
||||
@@ -21,14 +24,21 @@ Deno.test("workspace Worker list and sidebar attach through Worker Console hrefs
|
||||
);
|
||||
|
||||
assert(
|
||||
workspacePage.includes("workerConsoleHref(worker, workspaceId)") &&
|
||||
workspacePage.includes("Open Console"),
|
||||
"top Worker list should expose an attach action per Worker",
|
||||
!workspacePage.includes("workerConsoleHref") &&
|
||||
!workspacePage.includes("Open Console"),
|
||||
"top workspace page should not own the Worker list",
|
||||
);
|
||||
assert(
|
||||
workersNav.includes("workerConsoleHref(worker, workspaceId)") &&
|
||||
workersPage.includes("workerConsoleHref(worker, data.workspaceId)") &&
|
||||
workersPage.includes("<table class=\"workers-table\">") &&
|
||||
workersPage.includes("Open Console"),
|
||||
"dedicated Workers page should expose a table and attach action per Worker",
|
||||
);
|
||||
assert(
|
||||
workersNav.includes('href={`/w/${workspaceId}/workers`}') &&
|
||||
workersNav.includes("workerConsoleHref(worker, workspaceId)") &&
|
||||
workersNav.includes("aria-current"),
|
||||
"Workers sidebar rows should link to the Worker target Console route",
|
||||
"Workers sidebar should link to the Worker list page and target Console routes",
|
||||
);
|
||||
assert(
|
||||
!sidebar.includes("CompanionNavSection") &&
|
||||
|
||||
@@ -68,7 +68,14 @@
|
||||
|
||||
<section class="nav-section" aria-labelledby="workers-heading">
|
||||
<div class="section-heading-row">
|
||||
<h2 id="workers-heading">workers</h2>
|
||||
<h2 id="workers-heading">
|
||||
<a
|
||||
class="section-heading-link"
|
||||
class:active={currentPath === `/w/${workspaceId}/workers`}
|
||||
href={`/w/${workspaceId}/workers`}
|
||||
aria-current={currentPath === `/w/${workspaceId}/workers` ? 'page' : undefined}
|
||||
>workers</a>
|
||||
</h2>
|
||||
<a
|
||||
class="section-action"
|
||||
class:active={currentPath === `/w/${workspaceId}/workers/new`}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let workspaceId = $derived(data.workspace?.workspace_id ?? data.workspaceId);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -39,95 +37,48 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="grid runtime">
|
||||
<div class="card">
|
||||
<h2>Hosts</h2>
|
||||
{#if data.hosts}
|
||||
{#if data.hosts.items.length === 0}
|
||||
<p>No local Hosts are visible.</p>
|
||||
{:else}
|
||||
<div class="stack">
|
||||
{#each data.hosts.items as host}
|
||||
<article class="runtime-card">
|
||||
<div class="runtime-heading">
|
||||
<strong>{host.label}</strong>
|
||||
<span class:warn={host.status !== 'available'}>{host.status}</span>
|
||||
<section class="card">
|
||||
<h2>Hosts</h2>
|
||||
{#if data.hosts}
|
||||
{#if data.hosts.items.length === 0}
|
||||
<p>No local Hosts are visible.</p>
|
||||
{:else}
|
||||
<div class="stack">
|
||||
{#each data.hosts.items as host}
|
||||
<article class="runtime-card">
|
||||
<div class="runtime-heading">
|
||||
<strong>{host.label}</strong>
|
||||
<span class:warn={host.status !== 'available'}>{host.status}</span>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd><code>{host.host_id}</code></dd>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd><code>{host.host_id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Kind</dt>
|
||||
<dd>{host.kind}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Runtime</dt>
|
||||
<dd><code>{host.runtime_id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Scope</dt>
|
||||
<dd>{host.capabilities.workspace_scope}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Platform</dt>
|
||||
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if data.hostsError}
|
||||
<p class="error">{data.hostsError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/hosts</code>…</p>
|
||||
<div>
|
||||
<dt>Kind</dt>
|
||||
<dd>{host.kind}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Runtime</dt>
|
||||
<dd><code>{host.runtime_id}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Scope</dt>
|
||||
<dd>{host.capabilities.workspace_scope}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Platform</dt>
|
||||
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Workers</h2>
|
||||
{#if data.workers}
|
||||
{#if data.workers.items.length === 0}
|
||||
<p>No local Workers are visible.</p>
|
||||
{:else}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Worker</th>
|
||||
<th>Host</th>
|
||||
<th>State</th>
|
||||
<th>Workspace</th>
|
||||
<th>Implementation</th>
|
||||
<th>Attach</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.workers.items as worker}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{worker.label}</strong>
|
||||
{#if worker.role || worker.profile}
|
||||
<small>{worker.role ?? 'role unknown'} / {worker.profile ?? 'profile unknown'}</small>
|
||||
{/if}
|
||||
</td>
|
||||
<td><code>{worker.host_id}</code></td>
|
||||
<td>{worker.state} · {worker.status}</td>
|
||||
<td>{worker.workspace.visibility} · {worker.workspace.identity}</td>
|
||||
<td>{worker.implementation.kind}</td>
|
||||
<td><a class="inline-link" href={workerConsoleHref(worker, workspaceId)}>Open Console</a></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if data.workersError}
|
||||
<p class="error">{data.workersError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/workers</code>…</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if data.hostsError}
|
||||
<p class="error">{data.hostsError}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/hosts</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http";
|
||||
import type { Host, ListResponse, Worker } from "$lib/workspace-sidebar/types";
|
||||
import type { Host, ListResponse } from "$lib/workspace-sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
|
||||
const [hosts, workers] = await Promise.all([
|
||||
loadJson<ListResponse<Host>>(fetch, apiPath("/hosts")),
|
||||
loadJson<ListResponse<Worker>>(fetch, apiPath("/workers")),
|
||||
]);
|
||||
const hosts = await loadJson<ListResponse<Host>>(fetch, apiPath("/hosts"));
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
hosts: hosts.data,
|
||||
hostsError: hosts.error,
|
||||
workers: workers.data,
|
||||
workersError: workers.error,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import type { Worker } from '$lib/workspace-sidebar/types';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
function workerStatus(worker: Worker): string {
|
||||
return `${worker.state} · ${worker.status}`;
|
||||
}
|
||||
|
||||
function workerProfile(worker: Worker): string {
|
||||
return worker.profile ?? worker.role ?? 'unknown';
|
||||
}
|
||||
|
||||
function workerDirectory(worker: Worker): string {
|
||||
const directory = worker.working_directory;
|
||||
if (!directory) return '—';
|
||||
const selector = directory.requested_selector ?? 'HEAD';
|
||||
const commit = directory.resolved_commit ? directory.resolved_commit.slice(0, 12) : null;
|
||||
return commit ? `${directory.repository_id} · ${selector} · ${commit}` : `${directory.repository_id} · ${selector}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Workers · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Workers" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="workers-page" aria-labelledby="workers-heading">
|
||||
<header class="workers-page-header">
|
||||
<div>
|
||||
<h1 id="workers-heading">Workers</h1>
|
||||
<p>Workers running or persisted for this workspace.</p>
|
||||
</div>
|
||||
<a class="section-action" href={`/w/${data.workspaceId}/workers/new`}>New Worker</a>
|
||||
</header>
|
||||
|
||||
{#if data.workersError}
|
||||
<p class="section-state error">{data.workersError}</p>
|
||||
{:else if !data.workers}
|
||||
<p class="section-state">Loading Workers…</p>
|
||||
{:else if data.workers.items.length === 0}
|
||||
<p class="section-state">No Workers are visible.</p>
|
||||
{:else}
|
||||
<div class="table-wrap workers-table-wrap">
|
||||
<table class="workers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Worker</th>
|
||||
<th>Runtime</th>
|
||||
<th>Profile</th>
|
||||
<th>Status</th>
|
||||
<th>Working directory</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.workers.items as worker}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{worker.label}</strong>
|
||||
<small><code>{worker.worker_id}</code></small>
|
||||
</td>
|
||||
<td><code>{worker.runtime_id}</code></td>
|
||||
<td>{workerProfile(worker)}</td>
|
||||
<td>{workerStatus(worker)}</td>
|
||||
<td>{workerDirectory(worker)}</td>
|
||||
<td>
|
||||
<a class="inline-link" href={workerConsoleHref(worker, data.workspaceId)}>Open Console</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http";
|
||||
import type { ListResponse, Worker } from "$lib/workspace-sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const workers = await loadJson<ListResponse<Worker>>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/workers"),
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
workers: workers.data,
|
||||
workersError: workers.error,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user