97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
"use client"
|
|
|
|
// The project's runs, live (umbrella TS6.4 and TS6.6): the page's view renders the first page and hands it
|
|
// over as the seed, so the page arrives complete; this keeps it current through the organization's live
|
|
// stream -- a run another member starts appears, and each run's status and progress move as its steps
|
|
// commit, without a reload. The rows come from `agent.run.getMany`, under the same permission as the list,
|
|
// so the stream itself never carries a run's goal or plan.
|
|
|
|
import type { ProjectSlugs } from "@conusai-kit/ui"
|
|
import { liveCollection, useDataClient, useLive } from "@conusai-kit/ui/data"
|
|
import { PageSection, type ResourceColumn, ResourceList } from "@conusai-kit/ui/patterns"
|
|
import { type ReactElement, useState } from "react"
|
|
import { RUN_COLLECTION, type RunView } from "../contract/index.js"
|
|
|
|
export interface RunListLabels {
|
|
readonly heading: string
|
|
/** What to say when there is no run. */
|
|
readonly empty: string
|
|
readonly goal: string
|
|
readonly status: string
|
|
readonly progress: string
|
|
readonly startedBy: string
|
|
/** The message of each run status, by status. */
|
|
readonly statuses: Record<RunView["status"], string>
|
|
}
|
|
|
|
export function useLiveRuns(slugs: ProjectSlugs, seed: readonly RunView[]): readonly RunView[] {
|
|
const client = useDataClient()
|
|
// One collection for the life of the page; the seed is the first render's.
|
|
const [runs] = useState(() =>
|
|
liveCollection<RunView>({
|
|
client,
|
|
org: slugs.org,
|
|
topic: { ws: slugs.workspace, project: slugs.project },
|
|
entity: RUN_COLLECTION.entity,
|
|
key: RUN_COLLECTION.key,
|
|
seed,
|
|
list: {
|
|
procedure: RUN_COLLECTION.list,
|
|
input: { ...slugs },
|
|
rows: (out) => (out as { runs: RunView[] }).runs,
|
|
},
|
|
getMany: {
|
|
procedure: RUN_COLLECTION.getMany,
|
|
input: { ...slugs },
|
|
rows: (out) => (out as { items: RunView[] }).items,
|
|
},
|
|
}),
|
|
)
|
|
// Newest first, as the list procedure orders them.
|
|
const { data, isReady } = useLive(runs, (q, rows) =>
|
|
q.from({ run: rows }).orderBy(({ run }: { run: RunView }) => run.createdAt, "desc"),
|
|
)
|
|
// Until the collection is ready the server's rows are the answer, so the first paint matches it.
|
|
return isReady ? data : seed
|
|
}
|
|
|
|
export function RunList({
|
|
slugs,
|
|
seed,
|
|
labels,
|
|
}: {
|
|
slugs: ProjectSlugs
|
|
seed: readonly RunView[]
|
|
labels: RunListLabels
|
|
}): ReactElement {
|
|
const rows = useLiveRuns(slugs, seed)
|
|
const pageHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/agent`
|
|
const columns: ResourceColumn<RunView>[] = [
|
|
{ id: "goal", header: labels.goal, wrap: true },
|
|
{ id: "status", header: labels.status, cell: (run) => labels.statuses[run.status] },
|
|
{
|
|
id: "percent",
|
|
header: labels.progress,
|
|
cell: (run) => (
|
|
<progress value={run.percent} max={1}>
|
|
{Math.round(run.percent * 100)}%
|
|
</progress>
|
|
),
|
|
},
|
|
{ id: "actorId", header: labels.startedBy },
|
|
]
|
|
return (
|
|
<PageSection title={labels.heading}>
|
|
<ResourceList
|
|
label={labels.heading}
|
|
columns={columns}
|
|
data={rows}
|
|
rowId={(run) => run.runId}
|
|
rowHref={(run) => `${pageHref}/${run.runId}`}
|
|
rowLabel={(run) => run.goal}
|
|
empty={{ title: labels.empty }}
|
|
/>
|
|
</PageSection>
|
|
)
|
|
}
|