web: add workspace ticket routes
This commit is contained in:
@@ -12,10 +12,7 @@ export const load: LayoutLoad = async ({ fetch, params, url }) => {
|
||||
if (!workspaceId) {
|
||||
const workspace = await loadJson<WorkspaceResponse>(fetch, "/api/workspace");
|
||||
if (workspace.data) {
|
||||
const scopedPath = workspaceRoute(
|
||||
workspace.data.workspace_id,
|
||||
url.pathname === "/" ? "" : url.pathname,
|
||||
);
|
||||
const scopedPath = workspaceRoute(workspace.data.workspace_id);
|
||||
throw redirect(307, `${scopedPath}${url.search}`);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<main class="workspace-panel-shell">
|
||||
<section class="workspace-card">
|
||||
<h1>Redirecting to scoped workspace…</h1>
|
||||
<p class="section-note">This compatibility route bootstraps the current workspace id and redirects to the canonical <code>/w/<workspace-id></code> route.</p>
|
||||
<p class="section-note">The workspace entry bootstraps the current workspace id and opens the canonical <code>/w/<workspace-id></code> route.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<main class="objectives-page">
|
||||
<section class="workspace-card">
|
||||
<h1>Redirecting to scoped objectives…</h1>
|
||||
<p class="section-note">Objectives are available at the canonical <code>/w/<workspace-id>/objectives</code> route.</p>
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async () => ({});
|
||||
@@ -1,6 +0,0 @@
|
||||
<main class="objective-detail-page">
|
||||
<section class="workspace-card">
|
||||
<h1>Redirecting to scoped objective…</h1>
|
||||
<p class="section-note">Objective detail routes are canonical under <code>/w/<workspace-id>/objectives/<objective-id></code>.</p>
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async () => ({});
|
||||
@@ -1,6 +0,0 @@
|
||||
<main class="repository-detail-page">
|
||||
<section class="workspace-card">
|
||||
<h1>Redirecting to scoped repository…</h1>
|
||||
<p class="section-note">Repository routes are canonical under <code>/w/<workspace-id>/repositories/<repository-id></code>.</p>
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async () => ({});
|
||||
@@ -1,6 +0,0 @@
|
||||
<main class="worker-console-page">
|
||||
<section class="workspace-card">
|
||||
<h1>Redirecting to scoped Worker Console…</h1>
|
||||
<p class="section-note">Worker Console routes are canonical under <code>/w/<workspace-id>/runtimes/<runtime-id>/workers/<worker-id>/console</code>.</p>
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async () => ({});
|
||||
@@ -1,6 +0,0 @@
|
||||
<main class="settings-page">
|
||||
<section class="workspace-card">
|
||||
<h1>Redirecting to scoped settings…</h1>
|
||||
<p class="section-note">Settings are available at the canonical <code>/w/<workspace-id>/settings</code> route.</p>
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,8 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let workspaceId = $derived(data.workspace?.workspace_id ?? data.workspaceId);
|
||||
let ticketsHref = $derived(workspaceRoute(workspaceId, '/tickets'));
|
||||
let runtimeSettingsHref = $derived(workspaceRoute(workspaceId, '/settings/runtimes'));
|
||||
let workersHref = $derived(workspaceRoute(workspaceId, '/workers'));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -39,12 +43,17 @@
|
||||
</section>
|
||||
|
||||
<section class="workspace-actions" aria-label="Workspace sections">
|
||||
<a class="workspace-action-card" href={`/w/${workspaceId}/runtimes`}>
|
||||
<span>Runtimes</span>
|
||||
<strong>Manage runtimes and workdirs</strong>
|
||||
<a class="workspace-action-card" href={ticketsHref}>
|
||||
<span>Tickets</span>
|
||||
<strong>Browse workspace tickets</strong>
|
||||
<small>Read typed Ticket records</small>
|
||||
</a>
|
||||
<a class="workspace-action-card" href={runtimeSettingsHref}>
|
||||
<span>Runtime Inventory</span>
|
||||
<strong>Open admin runtime inventory</strong>
|
||||
<small>{data.hosts?.items.length ?? 0} host{(data.hosts?.items.length ?? 0) === 1 ? '' : 's'} visible</small>
|
||||
</a>
|
||||
<a class="workspace-action-card" href={`/w/${workspaceId}/workers`}>
|
||||
<a class="workspace-action-card" href={workersHref}>
|
||||
<span>Workers</span>
|
||||
<strong>Open worker list</strong>
|
||||
<small>Inspect status and attach to consoles</small>
|
||||
|
||||
+5
-5
@@ -10,15 +10,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Runtimes · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Runtimes" />
|
||||
<title>Runtime Inventory · Settings · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Runtime inventory" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="runtimes-page" aria-labelledby="runtimes-heading">
|
||||
<header class="page-header-row">
|
||||
<div>
|
||||
<h1 id="runtimes-heading">Runtimes</h1>
|
||||
<p>Execution backends available to this workspace.</p>
|
||||
<h1 id="runtimes-heading">Runtime Inventory</h1>
|
||||
<p>Admin view of execution backends available to this workspace.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<td>{runtimePlatform(runtime)}</td>
|
||||
<td>{runtime.capabilities.max_workers} workers</td>
|
||||
<td>
|
||||
<a class="inline-link" href={`/w/${data.workspaceId}/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs`}>
|
||||
<a class="inline-link" href={`/w/${data.workspaceId}/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs`}>
|
||||
Open workdirs
|
||||
</a>
|
||||
</td>
|
||||
+1
-1
@@ -105,7 +105,7 @@
|
||||
<section class="workdirs-page" aria-labelledby="workdirs-heading">
|
||||
<header class="page-header-row">
|
||||
<div>
|
||||
<p class="breadcrumb"><a href={`/w/${data.workspaceId}/runtimes`}>Runtimes</a> / {runtimeLabel}</p>
|
||||
<p class="breadcrumb"><a href={`/w/${data.workspaceId}/settings/runtimes`}>Runtime Inventory</a> / {runtimeLabel}</p>
|
||||
<h1 id="workdirs-heading">Workdirs</h1>
|
||||
<p>Workdirs owned by <code>{data.runtimeId}</code>.</p>
|
||||
{#if data.cleanupPlanError}<p class="section-state error">{data.cleanupPlanError}</p>{/if}
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Tickets · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Tickets" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="card">
|
||||
<div class="detail-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Workspace records</p>
|
||||
<h2>Tickets</h2>
|
||||
</div>
|
||||
{#if data.tickets.data}
|
||||
<span>{data.tickets.data.items.length} ticket{data.tickets.data.items.length === 1 ? '' : 's'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="section-note">
|
||||
Tickets are read from the typed Ticket backend. This surface is read-only; creation and queue operations remain outside the browser UI for now.
|
||||
</p>
|
||||
|
||||
{#if data.tickets.data}
|
||||
{#if data.tickets.data.items.length === 0}
|
||||
<p>No Ticket records are present.</p>
|
||||
{:else}
|
||||
<div class="ticket-list" aria-label="Workspace Tickets">
|
||||
{#each data.tickets.data.items as ticket (ticket.id)}
|
||||
<a class="ticket-row" href={workspaceRoute(data.workspaceId, `/tickets/${ticket.id}`)}>
|
||||
<div class="ticket-main">
|
||||
<div class="ticket-title-row">
|
||||
<strong class="ticket-title">{ticket.title}</strong>
|
||||
<span class="state-pill">{ticket.state}</span>
|
||||
</div>
|
||||
<p class="ticket-summary">
|
||||
{ticket.priority ? `${ticket.priority} priority` : 'priority unspecified'} · {ticket.record_source ?? 'ticket backend'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ticket-meta" aria-label="Ticket metadata">
|
||||
<span>Updated {ticket.updated_at ? formatDate(ticket.updated_at) : 'unknown'}</span>
|
||||
{#if ticket.queued_at}
|
||||
<span>Queued {formatDate(ticket.queued_at)}</span>
|
||||
{/if}
|
||||
<code>{ticket.id}</code>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.tickets.data.invalid_records.length > 0}
|
||||
<p class="error">{data.tickets.data.invalid_records.length} invalid Ticket record(s) hidden.</p>
|
||||
{/if}
|
||||
{:else if data.tickets.error}
|
||||
<p class="error">{data.tickets.error}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/w/{data.workspaceId}/tickets</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { TicketListResponse } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const tickets = await loadJson<TicketListResponse>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/tickets"),
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
tickets,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.ticket.data?.title ?? data.ticketId} · Tickets · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Ticket detail" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="card">
|
||||
<p class="breadcrumb"><a href={workspaceRoute(data.workspaceId, '/tickets')}>Tickets</a> / {data.ticketId}</p>
|
||||
|
||||
{#if data.ticket.data}
|
||||
<div class="detail-heading">
|
||||
<div>
|
||||
<p class="eyebrow">{data.ticket.data.id}</p>
|
||||
<h2>{data.ticket.data.title}</h2>
|
||||
</div>
|
||||
<span class="state-pill">{data.ticket.data.state}</span>
|
||||
</div>
|
||||
|
||||
<dl class="ticket-detail-grid">
|
||||
<div>
|
||||
<dt>Priority</dt>
|
||||
<dd>{data.ticket.data.priority ?? 'unspecified'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Updated</dt>
|
||||
<dd>{data.ticket.data.updated_at ? formatDate(data.ticket.data.updated_at) : 'unknown'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Created</dt>
|
||||
<dd>{data.ticket.data.created_at ? formatDate(data.ticket.data.created_at) : 'unknown'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Events</dt>
|
||||
<dd>{data.ticket.data.event_count}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Artifacts</dt>
|
||||
<dd>{data.ticket.data.artifact_count}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>{data.ticket.data.record_source}</dd>
|
||||
</div>
|
||||
{#if data.ticket.data.queued_at || data.ticket.data.queued_by}
|
||||
<div>
|
||||
<dt>Queued</dt>
|
||||
<dd>
|
||||
{data.ticket.data.queued_at ? formatDate(data.ticket.data.queued_at) : 'queued'}{data.ticket.data.queued_by ? ` by ${data.ticket.data.queued_by}` : ''}
|
||||
</dd>
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
|
||||
{#if data.ticket.data.risk_flags.length > 0}
|
||||
<div class="risk-flags" aria-label="Risk flags">
|
||||
{#each data.ticket.data.risk_flags as flag}
|
||||
<span>{flag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section class="ticket-body" aria-labelledby="ticket-body-heading">
|
||||
<div class="detail-heading compact">
|
||||
<h3 id="ticket-body-heading">Body</h3>
|
||||
{#if data.ticket.data.body_truncated}
|
||||
<span class="warning-pill">truncated</span>
|
||||
{/if}
|
||||
</div>
|
||||
<pre>{data.ticket.data.body || 'No body text is available.'}</pre>
|
||||
</section>
|
||||
{:else if data.ticket.error}
|
||||
<p class="error">{data.ticket.error}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/w/{data.workspaceId}/tickets/{data.ticketId}</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { TicketDetail } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const ticketId = params.ticketId;
|
||||
const ticket = await loadJson<TicketDetail>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, `/tickets/${encodeURIComponent(ticketId)}`),
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
ticketId,
|
||||
ticket,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
Reference in New Issue
Block a user