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
6 changes: 4 additions & 2 deletions packages/api/test/static-slice.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.mock("@cloudflare/workers-oauth-provider", () => ({ OAuthProvider: class {},
vi.mock("cloudflare:workers", () => ({ WorkerEntrypoint: class {}, DurableObject: class {} }));
const url = process.env.AUTH_TEST_DATABASE_URL;
describe.skipIf(!url)("compiled React + actual Worker API + PostgreSQL", () => {
it("authenticates, navigates a project's jobs, invokes once, refreshes the run and resets a projected trace", async () => {
it("authenticates, navigates a project's runs, invokes once, refreshes the run and resets a projected trace", async () => {
const f = await loadingFixture(url!), browser = await chromium.launch({ headless: true });
const failures: string[] = [], calls: string[] = [];
try {
Expand Down Expand Up @@ -71,7 +71,9 @@ describe.skipIf(!url)("compiled React + actual Worker API + PostgreSQL", () => {
expect(await page.getByRole("link", { name: "Factorize", exact: true }).count()).toBe(0);
expect(await page.locator("html").getAttribute("data-theme")).toBe("light");
expect(await page.locator("body").evaluate((element: any) => (globalThis as any).getComputedStyle(element).backgroundColor)).toBe("rgb(252, 251, 246)");
await page.getByRole("link", { name: "Job-1", exact: true }).click();
expect(await page.getByRole("navigation", { name: "Project navigation" }).getByRole("link", { name: "Runs", exact: true }).count()).toBe(0);
await page.getByRole("link", { name: "Job-0", exact: true }).first().waitFor();
await page.goto(`https://factorize.test/jobs/${f.jobIds[1]}`);
await page.getByRole("button", { name: "More run options", exact: true }).click();
await page.getByRole("menuitem", { name: "Run with prompt", exact: true }).click();
await page.getByLabel("Prompt", { exact: true }).fill("Real invocation");
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/features/projects/detail.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link, useParams } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { ActionBar, Button } from "../../shared/ui";
import { JobsTable } from "../jobs/list";
import { RunList } from "../runs/list";
import { ProjectGraph } from "./graph";
import { projectQuery } from "./queries";
import { ProjectNav } from "./project-nav";
Expand All @@ -12,10 +12,10 @@ export function ProjectDetail() {
const project = useQuery(projectQuery(projectId));
const [view, setView] = useState<"list" | "graph">("list");
return <ProjectNav tab="jobs">{project.data && <ActionBar>
<div role="group" aria-label="Project jobs view" className="inline-flex rounded-lg border border-stone-200 bg-white p-0.5 dark:border-slate-700 dark:bg-slate-900">
<div role="group" aria-label="Project view" className="inline-flex rounded-lg border border-stone-200 bg-white p-0.5 dark:border-slate-700 dark:bg-slate-900">
<Button className={`min-h-9 rounded-md border-0 px-3 py-1.5 ${view === "list" ? "bg-stone-100 text-slate-950 shadow-sm dark:bg-slate-800 dark:text-white" : "bg-transparent text-slate-600 hover:bg-stone-50 dark:text-slate-300 dark:hover:bg-slate-800"}`} aria-pressed={view === "list"} onClick={() => setView("list")}>List</Button>
<Button className={`min-h-9 rounded-md border-0 px-3 py-1.5 ${view === "graph" ? "bg-stone-100 text-slate-950 shadow-sm dark:bg-slate-800 dark:text-white" : "bg-transparent text-slate-600 hover:bg-stone-50 dark:text-slate-300 dark:hover:bg-slate-800"}`} aria-pressed={view === "graph"} onClick={() => setView("graph")}>Graph</Button>
</div>
<Link to="/jobs/new" search={{ projectId }}><Button variant="primary">Create job</Button></Link>
</ActionBar>}{view === "list" ? <JobsTable projectId={projectId} /> : <ProjectGraph projectId={projectId} />}</ProjectNav>;
</ActionBar>}{view === "list" ? <RunList projectId={projectId} limit={20} /> : <ProjectGraph projectId={projectId} />}</ProjectNav>;
}
7 changes: 3 additions & 4 deletions packages/app/src/features/projects/project-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import { Page } from "../../shared/ui";
import { NavIcon } from "../../shared/nav-icon";
import { projectQuery } from "./queries";

type ProjectTab = "jobs" | "runs" | "settings";
type ProjectTab = "jobs" | "settings";

const tabs: Array<{ id: ProjectTab; label: string; to: "/projects/$projectId" | "/projects/$projectId/runs" | "/projects/$projectId/settings"; icon: React.ReactNode }> = [
const tabs: Array<{ id: ProjectTab; label: string; to: "/projects/$projectId" | "/projects/$projectId/settings"; icon: React.ReactNode }> = [
{ id: "jobs", label: "Jobs", to: "/projects/$projectId", icon: <NavIcon name="jobs" /> },
{ id: "runs", label: "Runs", to: "/projects/$projectId/runs", icon: <NavIcon name="runs" /> },
{ id: "settings", label: "Settings", to: "/projects/$projectId/settings", icon: <NavIcon name="settings" /> },
];

Expand All @@ -24,7 +23,7 @@ export function ProjectNav({ tab, projectId: projectIdProp, children }: { tab: P
</div>
</nav>
<Page className="pt-6 sm:pt-8">
<h1 className="sr-only">{project.data?.name ?? "Project"} {tab === "jobs" ? "jobs" : tab === "runs" ? "runs" : "settings"}</h1>
<h1 className="sr-only">{project.data?.name ?? "Project"} {tab === "jobs" ? "runs" : "settings"}</h1>
{project.isPending && <p role="status">Loading project…</p>}{project.error && <p role="alert">{project.error.message}</p>}
{children}
</Page>
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/features/projects/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createRoute, lazyRouteComponent, redirect, type AnyRootRoute } from "@tanstack/react-router";
import { queryClient } from "../../shared/query-client"; import { sessionQuery } from "../auth/session"; import { projectsQuery, projectQuery } from "./queries";
import { jobsQuery, type JobsSearch } from "../jobs/queries";
export function projectsRoutes(root: AnyRootRoute) { const guard=async({location}:{location:{href:string}})=>{if(!(await queryClient.ensureQueryData(sessionQuery)).authenticated)throw redirect({to:"/auth/login",search:{returnTo:location.href,token:undefined}});}; const list=createRoute({getParentRoute:()=>root,path:"/projects",beforeLoad:guard,loader:()=>queryClient.ensureQueryData(projectsQuery),component:lazyRouteComponent(()=>import("./list"),"ProjectsList")}); const create=createRoute({getParentRoute:()=>root,path:"/projects/new",beforeLoad:guard,component:lazyRouteComponent(()=>import("./create"),"ProjectCreate")}); const validateJobsSearch = (s: Record<string, unknown>): JobsSearch => ({ q: typeof s.q === "string" ? s.q.slice(0, 120) : "", cursor: typeof s.cursor === "string" ? s.cursor : undefined, enabled: s.enabled === "true" || s.enabled === true ? "true" : s.enabled === "false" || s.enabled === false ? "false" : undefined, sort: s.sort === "running" || s.sort === "title" ? s.sort : undefined, order: s.order === "asc" || s.order === "desc" ? s.order : undefined }); const detail=createRoute({getParentRoute:()=>root,path:"/projects/$projectId",beforeLoad:guard,validateSearch:validateJobsSearch,loaderDeps:({search})=>({q:search.q,cursor:search.cursor,enabled:search.enabled,sort:search.sort,order:search.order}),loader:({params,deps})=>{void queryClient.prefetchQuery(jobsQuery({...deps,projectId:params.projectId})).catch(()=>undefined);return queryClient.ensureQueryData(projectQuery(params.projectId));},component:lazyRouteComponent(()=>import("./detail"),"ProjectDetail")}); const projectRuns=createRoute({getParentRoute:()=>root,path:"/projects/$projectId/runs",beforeLoad:guard,validateSearch:(s: Record<string, unknown>) => ({cursor:typeof s.cursor === "string" ? s.cursor : undefined, previous:typeof s.previous === "string" ? s.previous : undefined, projects:undefined, jobs:Array.isArray(s.jobs) ? s.jobs.filter((job): job is string => typeof job === "string") : typeof s.jobs === "string" ? [s.jobs] : undefined, states:Array.isArray(s.states) ? s.states : undefined, sort:typeof s.sort === "string" ? s.sort : undefined, direction:s.direction === "asc" || s.direction === "desc" ? s.direction : undefined}),loader:({params})=>queryClient.ensureQueryData(projectQuery(params.projectId)),component:lazyRouteComponent(()=>import("./runs"),"ProjectRuns")}); const settings=createRoute({getParentRoute:()=>root,path:"/projects/$projectId/settings",beforeLoad:guard,loader:({params})=>queryClient.ensureQueryData(projectQuery(params.projectId)),component:lazyRouteComponent(()=>import("./settings"),"ProjectSettings")}); return [list,create,detail,projectRuns,settings]; }
import { jobsQuery } from "../jobs/queries";
import { runPageQuery } from "../runs/queries";
import type { RunListSearch } from "../runs/list";
export function projectsRoutes(root: AnyRootRoute) { const guard=async({location}:{location:{href:string}})=>{if(!(await queryClient.ensureQueryData(sessionQuery)).authenticated)throw redirect({to:"/auth/login",search:{returnTo:location.href,token:undefined}});}; const list=createRoute({getParentRoute:()=>root,path:"/projects",beforeLoad:guard,loader:()=>queryClient.ensureQueryData(projectsQuery),component:lazyRouteComponent(()=>import("./list"),"ProjectsList")}); const create=createRoute({getParentRoute:()=>root,path:"/projects/new",beforeLoad:guard,component:lazyRouteComponent(()=>import("./create"),"ProjectCreate")}); const validateProjectRunsSearch = (s: Record<string, unknown>): RunListSearch => ({ cursor:typeof s.cursor === "string" ? s.cursor : undefined, previous:typeof s.previous === "string" ? s.previous : undefined, jobs:Array.isArray(s.jobs) ? s.jobs.filter((job): job is string => typeof job === "string") : typeof s.jobs === "string" ? [s.jobs] : undefined, states:Array.isArray(s.states) ? s.states as RunListSearch["states"] : typeof s.states === "string" ? [s.states as NonNullable<RunListSearch["states"]>[number]] : undefined, sort:s.sort === "job" || s.sort === "run" || s.sort === "status" || s.sort === "created" || s.sort === "agent" ? s.sort : undefined, direction:s.direction === "asc" || s.direction === "desc" ? s.direction : undefined }); const loadProject = ({params,deps}:{params:{projectId:string};deps:RunListSearch}) => { void queryClient.prefetchQuery(jobsQuery({q:"",projectId:params.projectId})).catch(()=>undefined); void queryClient.prefetchQuery(runPageQuery({projectIds:[params.projectId],jobIds:deps.jobs,cursor:deps.cursor,limit:20,sort:deps.sort,direction:deps.direction,states:deps.states})).catch(()=>undefined); return queryClient.ensureQueryData(projectQuery(params.projectId)); }; const detail=createRoute({getParentRoute:()=>root,path:"/projects/$projectId",beforeLoad:guard,validateSearch:validateProjectRunsSearch,loaderDeps:({search})=>search,loader:loadProject,component:lazyRouteComponent(()=>import("./detail"),"ProjectDetail")}); const projectRuns=createRoute({getParentRoute:()=>root,path:"/projects/$projectId/runs",beforeLoad:guard,validateSearch:validateProjectRunsSearch,loaderDeps:({search})=>search,loader:loadProject,component:lazyRouteComponent(()=>import("./runs"),"ProjectRuns")}); const settings=createRoute({getParentRoute:()=>root,path:"/projects/$projectId/settings",beforeLoad:guard,loader:({params})=>queryClient.ensureQueryData(projectQuery(params.projectId)),component:lazyRouteComponent(()=>import("./settings"),"ProjectSettings")}); return [list,create,detail,projectRuns,settings]; }
2 changes: 1 addition & 1 deletion packages/app/src/features/projects/runs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useParams } from "@tanstack/react-router";
import { RunList } from "../runs/list";
import { ProjectNav } from "./project-nav";
export function ProjectRuns() { const { projectId } = useParams({ from: "/projects/$projectId/runs" }); return <ProjectNav tab="runs"><h2 className="sr-only">Project runs</h2><RunList projectId={projectId} limit={20} /></ProjectNav>; }
export function ProjectRuns() { const { projectId } = useParams({ from: "/projects/$projectId/runs" }); return <ProjectNav tab="jobs"><h2 className="sr-only">Project runs</h2><RunList projectId={projectId} limit={20} /></ProjectNav>; }
Loading
Loading