From c052cc55646731fad9be84f50d6d2d289a735733 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 14:27:09 -0700 Subject: [PATCH] fix(web): the workflow detail page matches the mockup's routine layout (CL-9666) --- apps/web/src/pages/routine-detail-page.tsx | 99 +++++++++++++++------- apps/web/src/pages/routine-ui.tsx | 18 +++- apps/web/src/pages/routines.css | 55 ++++-------- 3 files changed, 102 insertions(+), 70 deletions(-) diff --git a/apps/web/src/pages/routine-detail-page.tsx b/apps/web/src/pages/routine-detail-page.tsx index aef426d3b..7f19513db 100644 --- a/apps/web/src/pages/routine-detail-page.tsx +++ b/apps/web/src/pages/routine-detail-page.tsx @@ -1,4 +1,4 @@ -import { PageShell, RichEmptyState, RunNowButton, Skeleton } from "@corbits/react-ui"; +import { RichEmptyState, RunNowButton, Skeleton } from "@corbits/react-ui"; import { type } from "arktype"; import { useState } from "react"; import { Clock } from "@/lib/icons"; @@ -9,12 +9,14 @@ import type { GlobalRoutineRow } from "../global-routines"; import { useNavigate } from "../navigation"; import { WORKFLOWS_PATH_PREFIX } from "../path-ids"; import { PageLayout } from "../shell/page-layout"; +import { ListCard } from "./library-list"; import { StageTopBar } from "../shell/stage-top-bar"; import { PauseResumeButton, RoutinePill, formatWhen, routineState, + useRoutineDescription, useRoutineRuns, } from "./routine-ui"; import type { RunRow } from "./routine-ui"; @@ -39,6 +41,8 @@ function runLabel(status: RunRow["status"]): string { return status === "deployed" ? "ok" : status; } +const FAILED_WITHOUT_MESSAGE = "The run failed without reporting an error."; + /** The run's own failure text: the newest error-ish event's message. */ function failureMessage(events: readonly RunEvent[]): string | null { for (const event of events.toReversed()) { @@ -51,6 +55,23 @@ function failureMessage(events: readonly RunEvent[]): string | null { return null; } +/** The Result cell: what the run came to, the failure's own message when it failed. */ +function RunResult({ tenantId, run }: { readonly tenantId: string; readonly run: RunRow }) { + const eventsQuery = useAPIQuery( + run.status === "error" ? runEventsPath(tenantId, run.id) : "", + RunEventsSchema, + ); + if (run.status === "error") { + const failure = + eventsQuery.kind === "ready" + ? (failureMessage(eventsQuery.data.events) ?? FAILED_WITHOUT_MESSAGE) + : "…"; + return {failure}; + } + if (run.status === "running" || run.status === "updating") return Running…; + return {run.status === "deployed" ? "Succeeded" : run.status}; +} + function RunDetail({ tenantId, run, @@ -72,7 +93,7 @@ function RunDetail({
{run.status === "error" ? ( <> -

{failure ?? "The run failed without reporting an error."}

+

{failure ?? FAILED_WITHOUT_MESSAGE}

@@ -116,29 +137,33 @@ function RoutineRunsSection({ ) : null} {runsQuery.kind === "ready" && runsQuery.data.data.length > 0 ? ( -
+ {runsQuery.data.data.map((run) => ( -
+
  • + + + + + {runLabel(run.status)} + {selectedRunId === run.id ? ( ) : null} -
  • + ))} -
    + ) : null} ); @@ -184,6 +209,8 @@ export function RoutineDetailPage({ const runs = useRoutineRuns(row.tenantId, row.definition.definitionId, 1); const lastRun = runs.kind === "ready" ? runs.data.data[0] : undefined; const state = routineState(row, lastRun); + const description = useRoutineDescription(row.tenantId, row.definition.name); + const paused = state.label === "paused"; return (
    - - -
    - } /> - -
    -
    -

    {row.definition.name}

    - {state.label} -
    +
    + + {sentence} + {state.label} + + } + actions={ + <> + + + + } + >
    Schedule
    {sentence}
    -
    Workbench
    -
    {row.tenantName}
    +
    Delivers to
    +
    {row.tenantName === "" ? "This workbench" : row.tenantName}
    Last run
    {runs.kind === "ready" ? formatWhen(lastRun?.createdAt) : "…"}
    +
    Next run
    +
    {paused ? "Paused" : sentence}
    + {description !== null ? ( +
    +

    What it does

    +

    {description}

    +
    + ) : null} -
    - + +
    ); } diff --git a/apps/web/src/pages/routine-ui.tsx b/apps/web/src/pages/routine-ui.tsx index 8b9565a20..edbd01e08 100644 --- a/apps/web/src/pages/routine-ui.tsx +++ b/apps/web/src/pages/routine-ui.tsx @@ -3,7 +3,7 @@ import "./routines.css"; import { Button } from "@corbits/react-ui"; import { toast } from "@corbits/react-ui/ui/toast"; import { reportError } from "@corbits/error-sink"; -import { WorkflowRunResponse, paginatedSchema } from "@intx/types"; +import { WorkflowDefinitionResponse, WorkflowRunResponse, paginatedSchema } from "@intx/types"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { describeApiError } from "@/lib/api-query"; import { tenantKeys } from "../query-client"; @@ -27,6 +27,22 @@ export function useRoutineRuns( return useAPIQuery(runsPath(tenantId, definitionId, limit), RunsPageSchema); } +const DefinitionsPageSchema = paginatedSchema(WorkflowDefinitionResponse); + +/** The definition's own description, joined to the deployment by name (the + * same join the cron row uses); null while loading or when it has none. */ +export function useRoutineDescription(tenantId: string, name: string): string | null { + const query = useAPIQuery( + `/api/tenants/${tenantId}/workflows/definitions?limit=100`, + DefinitionsPageSchema, + ); + if (query.kind !== "ready") return null; + const description = query.data.data.find((definition) => definition.name === name)?.description; + return description === undefined || description === null || description === "" + ? null + : description; +} + export function formatWhen(iso: string | null | undefined): string { if (iso === null || iso === undefined) return "Never"; const date = new Date(iso); diff --git a/apps/web/src/pages/routines.css b/apps/web/src/pages/routines.css index 2c94bb37e..5938bebbd 100644 --- a/apps/web/src/pages/routines.css +++ b/apps/web/src/pages/routines.css @@ -1,13 +1,3 @@ -.routine-rows { - list-style: none; - margin: 0; - padding: 0; - background: var(--surface); - border-radius: var(--r-lg); - box-shadow: var(--raised); - overflow: hidden; -} - .routine-meta { color: var(--ink-2); overflow: hidden; @@ -54,22 +44,18 @@ background: var(--info); } -.routine-head { - display: flex; +.routine-sub { + display: inline-flex; align-items: center; - gap: 12px; -} - -.routine-head h1 { - font-family: var(--font-mono); - font-size: 24px; + flex-wrap: wrap; + gap: 10px; } .routine-kv { display: grid; grid-template-columns: 140px 1fr; gap: 10px 16px; - margin: 0; + margin: 0 0 24px; font-size: 13.5px; } .routine-kv dt { @@ -81,6 +67,14 @@ min-width: 0; } +.routine-sec { + margin-bottom: 24px; +} +.routine-prose { + margin: 0; + color: var(--ink-2); + font-size: 13.5px; +} .routine-sec h2 { margin: 0 0 8px; font-size: 12.5px; @@ -88,37 +82,20 @@ color: var(--ink-2); } -.routine-run { - border-bottom: 1px solid var(--line); -} -.routine-run:last-child { - border-bottom: 0; -} - -.routine-run-summary { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - align-items: center; - gap: 16px; - width: 100%; - padding: 12px 16px; +.routine-run-when { + padding: 0; border: 0; background: none; - color: inherit; font: inherit; - font-size: 13.5px; text-align: left; cursor: pointer; } -.routine-run-summary:hover { - background: var(--surface-sunk); -} .routine-run-detail { display: flex; flex-direction: column; gap: 8px; - padding: 0 16px 14px; + grid-column: 1 / -1; } .routine-error {