Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions apps/web/src/pages/bench-insights.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
.bi-head {
margin-bottom: 1rem;
/* Card and table labels read as sentence case, like the drawer's tabs. */
.page-layout .insights-panel h3 {
font-size: 0.9rem;
letter-spacing: 0;
text-transform: none;
color: var(--ink);
}
.bi-head h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
.bi-head p {
margin: 0.25rem 0 0;
color: var(--muted-foreground);
.page-layout .bi-table th {
font-size: 0.78rem;
letter-spacing: 0;
text-transform: none;
}
.bi-other {
background: var(--muted-foreground);
Expand Down
11 changes: 4 additions & 7 deletions apps/web/src/pages/bench-insights.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The bench-scoped Insights dashboard: every number is computed from the
// stock `GET /workflows/runs` listing of the bench's own tenant.

import { Badge, PageShell, RichEmptyState, Skeleton, RUN_STATUS_TONE } from "@corbits/react-ui";
import { Badge, RichEmptyState, Skeleton, RUN_STATUS_TONE } from "@corbits/react-ui";
import { useState, type ReactNode } from "react";

import { useAPIQuery } from "../api";
Expand All @@ -17,6 +17,7 @@ import {
} from "../insights-stats";
import { formatWhen } from "./insights-page";
import { useFromBench } from "../shell/page-crumbs";
import { PageLayout } from "../shell/page-layout";
import { StageTopBar } from "../shell/stage-top-bar";
import { workbenchPath } from "../workbench-path";

Expand Down Expand Up @@ -207,13 +208,9 @@ export function BenchInsights({
<div className="flex h-full min-h-0 flex-col">
<StageTopBar crumbs={crumbs} actions={rangeSeg} />
<div className="min-h-0 flex-1 overflow-y-auto">
<PageShell width="full" className="page-fill">
<div className="bi-head">
<h1>Insights</h1>
<p>What happened in {title}.</p>
</div>
<PageLayout title="Insights" subtitle={`What happened in ${title}.`}>
{body}
</PageShell>
</PageLayout>
</div>
</div>
);
Expand Down
28 changes: 16 additions & 12 deletions apps/web/src/pages/routine-detail-page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { PageShell, RichEmptyState, RunNowButton, Skeleton } from "@corbits/react-ui";
import { type } from "arktype";
import { useState } from "react";
import type { ReactNode } from "react";
import { Clock } from "@/lib/icons";

import { useAPIQuery } from "../api";
import { useGlobalRoutines, useRoutineActions } from "../global-routines";
import type { GlobalRoutineRow } from "../global-routines";
import { Link } from "../navigation";
import { useNavigate } from "../navigation";
import { WORKFLOWS_PATH_PREFIX } from "../path-ids";
import { PageLayout } from "../shell/page-layout";
import { StageTopBar } from "../shell/stage-top-bar";
import { RoutinePill, formatWhen, routineState, useRoutineRuns } from "./routine-ui";
import type { RunRow } from "./routine-ui";
Expand Down Expand Up @@ -140,25 +141,28 @@ function RoutineRunsSection({
function RoutineNotice({
title,
description,
children,
}: {
readonly title: string;
readonly description: string;
readonly children?: ReactNode;
}) {
const navigate = useNavigate();
return (
<div className="flex h-full min-h-0 flex-col">
<StageTopBar
crumbs={[{ label: "Workflows", href: WORKFLOWS_PATH_PREFIX }, { label: title }]}
/>
<PageShell>
<p className="m-0 text-sm text-[var(--ui-fg-muted)]">{description}</p>
{children ?? (
<p className="mt-4">
<Link to={WORKFLOWS_PATH_PREFIX}>Back to Workflows</Link>
</p>
)}
</PageShell>
<div className="min-h-0 flex-1 overflow-y-auto">
<PageLayout title={title}>
<RichEmptyState
icon={<Clock />}
title="Workflow unavailable"
description={description}
actions={[
{ label: "Back to Workflows", onClick: () => navigate(WORKFLOWS_PATH_PREFIX) },
]}
/>
</PageLayout>
</div>
</div>
);
}
Expand Down
40 changes: 25 additions & 15 deletions apps/web/src/pages/routines-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// paused (`stopped`) ones. Run-now is the only write.
import { EmptyState, RichEmptyState, RunNowButton } from "@corbits/react-ui";
import { cronSentence } from "@corbits/workflows/client";
import { useState } from "react";
import { useState, type ReactNode } from "react";
import { Clock } from "@/lib/icons";

import { useGlobalRoutines, useRoutineActions } from "../global-routines";
Expand Down Expand Up @@ -57,6 +57,17 @@ function RoutineListRow({
);
}

function WorkflowsLayout({ children }: { readonly children: ReactNode }) {
return (
<PageLayout
title="Workflows"
subtitle="Deployed workflows, when they run, and how the last run went."
>
{children}
</PageLayout>
);
}

export function GlobalRoutinesList({
rows,
onRunNow,
Expand All @@ -68,11 +79,13 @@ export function GlobalRoutinesList({
const fromId = useFromBench();
if (rows.length === 0) {
return (
<RichEmptyState
icon={<Clock />}
title="No workflows yet"
description="A deployed workflow shows up here. Run it now from its row."
/>
<WorkflowsLayout>
<RichEmptyState
icon={<Clock />}
title="No workflows yet"
description="A deployed workflow shows up here. Run it now from its row."
/>
</WorkflowsLayout>
);
}
const needle = query.trim().toLowerCase();
Expand All @@ -82,10 +95,7 @@ export function GlobalRoutinesList({
.includes(needle),
);
return (
<PageLayout
title="Workflows"
subtitle="Deployed workflows, when they run, and how the last run went."
>
<WorkflowsLayout>
<input
className="routines-filter"
placeholder="Filter workflows"
Expand All @@ -103,7 +113,7 @@ export function GlobalRoutinesList({
/>
))}
</ul>
</PageLayout>
</WorkflowsLayout>
);
}

Expand All @@ -117,17 +127,17 @@ export function RoutinesRoute() {
<StageTopBar crumbs={[{ label: "Workflows" }]} />
<div className="stage-content flex min-h-0 flex-1 flex-col overflow-y-auto">
{routinesQuery.kind === "loading" ? (
<div className="flex flex-1 items-center justify-center p-6">
<WorkflowsLayout>
<EmptyState icon={<Clock />} title="Loading workflows…" />
</div>
</WorkflowsLayout>
) : routinesQuery.kind === "error" ? (
<div className="flex flex-1 items-center justify-center p-6">
<WorkflowsLayout>
<RichEmptyState
icon={<Clock />}
title="Couldn't load workflows"
description={routinesQuery.message}
/>
</div>
</WorkflowsLayout>
) : (
<GlobalRoutinesList rows={rows} onRunNow={(row) => actions.runNow(row)} />
)}
Expand Down
124 changes: 72 additions & 52 deletions apps/web/src/shell/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useNavigate } from "../navigation";
import { saveArtifactContent } from "./library-artifacts";
import { APP_ROUTES, matchesRoute } from "../routes";
import type { SessionUser } from "../session";
import { isWorkbenchPath } from "../workbench-path";
import { StageTopBar } from "./stage-top-bar";
import {
useCanvasColumnArtifact,
Expand All @@ -26,6 +27,7 @@ import {
useToggleCanvasFocus,
} from "./canvas-availability";
import { Sidebar } from "./sidebar";
import { isNarrow, SidebarToggleContext, useIsNarrow } from "./sidebar-toggle";
import { ShellContextMenu } from "./context-menu/shell-context-menu";
import { FirstRunTour } from "./first-run-tour";
import "./shell-narrow.css";
Expand Down Expand Up @@ -126,6 +128,16 @@ export function AppShell({
// Keyed by path so any navigation closes the phone-width sidebar.
const [sidebarOpenFor, setSidebarOpenFor] = useState<string | null>(null);
const sidebarOpen = sidebarOpenFor === path;
// Desktop collapse. The chat and home screens carry no `StageTopBar`, so
// they always show the sidebar rather than leave no way back.
const [collapsed, setCollapsed] = useState(false);
const narrow = useIsNarrow();
const collapsible = path !== "/" && !isWorkbenchPath(path);
const sidebarCollapsed = collapsed && collapsible;
const toggleSidebar = () => {
if (isNarrow()) setSidebarOpenFor(sidebarOpen ? null : path);
else setCollapsed(!collapsed);
};
useEffect(() => {
if (!sidebarOpen) return;
const onKey = (event: KeyboardEvent) => {
Expand All @@ -146,61 +158,69 @@ export function AppShell({
: { tone: "ok" as const, label: "All caught up" };

return (
<div className="shell-frame" data-sidebar-open={sidebarOpen}>
<Button
variant="ghost"
size="sm"
className="shell-sidebar-toggle"
aria-label="Toggle sidebar"
aria-expanded={sidebarOpen}
onClick={() => setSidebarOpenFor(sidebarOpen ? null : path)}
>
<PanelLeft />
</Button>
<SidebarToggleContext
value={{ expanded: narrow ? sidebarOpen : !sidebarCollapsed, toggle: toggleSidebar }}
>
<div
className="shell-sidebar-scrim"
onClick={() => setSidebarOpenFor(null)}
aria-hidden="true"
/>
<Sidebar path={path} onNavigate={navigate} />
<div className="shell-main" ref={mainRef}>
<ScrollToTop key={path} containerRef={mainRef} />
<div className="shell-main-content">
{routeHasNoStageTopBar(path) ? (
<StageTopBar
crumbs={[{ label: routeLabel(path) }]}
{...(pendingChip !== undefined ? { chip: pendingChip } : {})}
className="shell-frame"
data-sidebar-open={sidebarOpen}
data-sidebar-collapsed={sidebarCollapsed}
>
<Button
variant="ghost"
size="sm"
className="shell-sidebar-toggle"
aria-label="Toggle sidebar"
aria-expanded={sidebarOpen}
onClick={toggleSidebar}
>
<PanelLeft />
</Button>
<div
className="shell-sidebar-scrim"
onClick={() => setSidebarOpenFor(null)}
aria-hidden="true"
/>
<Sidebar path={path} onNavigate={navigate} />
<div className="shell-main" ref={mainRef}>
<ScrollToTop key={path} containerRef={mainRef} />
<div className="shell-main-content">
{routeHasNoStageTopBar(path) ? (
<StageTopBar
crumbs={[{ label: routeLabel(path) }]}
{...(pendingChip !== undefined ? { chip: pendingChip } : {})}
/>
) : null}
<Suspense
fallback={
<div className="page-fill shell-route-loading">
<WorkbenchLoadingState />
</div>
}
>
{children}
</Suspense>
</div>
</div>
{canvasAllowed ? (
<Suspense fallback={null}>
<CanvasColumn
open={canvasOpen}
profile={canvasProfile}
artifact={canvasArtifact}
routine={canvasRoutine}
focus={canvasFocus}
onClose={closeCanvas}
onToggleFocus={toggleCanvasFocus}
onNavigate={navigate}
artifactSaveState={artifactSaveState}
onSaveArtifact={saveArtifact}
/>
) : null}
<Suspense
fallback={
<div className="page-fill shell-route-loading">
<WorkbenchLoadingState />
</div>
}
>
{children}
</Suspense>
</div>
) : null}
<ShellContextMenu onSignOut={onSignOut} />
<FirstRunTour userId={user.id} />
</div>
{canvasAllowed ? (
<Suspense fallback={null}>
<CanvasColumn
open={canvasOpen}
profile={canvasProfile}
artifact={canvasArtifact}
routine={canvasRoutine}
focus={canvasFocus}
onClose={closeCanvas}
onToggleFocus={toggleCanvasFocus}
onNavigate={navigate}
artifactSaveState={artifactSaveState}
onSaveArtifact={saveArtifact}
/>
</Suspense>
) : null}
<ShellContextMenu onSignOut={onSignOut} />
<FirstRunTour userId={user.id} />
</div>
</SidebarToggleContext>
);
}
9 changes: 9 additions & 0 deletions apps/web/src/shell/page-crumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ export function usePageCrumbs(crumbs: readonly PageCrumb[]): {
readonly back: { readonly href: string; readonly name: string } | null;
} {
const from = useFromWorkbench();
const fromId = useFromBench();
// A bare one-level page reads "Workbench / <Page>"; `?from=` swaps the
// root for the bench itself.
if (fromId === null) {
return {
crumbs: crumbs.length === 1 ? [{ label: "Workbench" }, ...crumbs] : crumbs,
back: null,
};
}
if (from === null || from.name === null) return { crumbs, back: null };
const href = workbenchPath(from.id);
return {
Expand Down
Loading
Loading