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 {