diff --git a/web/bun.lockb b/web/bun.lockb
index 5e71c92..da357e6 100755
Binary files a/web/bun.lockb and b/web/bun.lockb differ
diff --git a/web/package.json b/web/package.json
index 0fd9a24..c075a23 100644
--- a/web/package.json
+++ b/web/package.json
@@ -66,6 +66,7 @@
"next-plausible": "^3.12.0",
"next-themes": "^0.2.1",
"next-usequerystate": "^1.13.2",
+ "pg": "^8.11.3",
"react": "^18",
"react-day-picker": "^8.9.1",
"react-dom": "^18",
diff --git a/web/src/app/(app)/api-keys/loading.tsx b/web/src/app/(app)/api-keys/loading.tsx
new file mode 100644
index 0000000..9ff4783
--- /dev/null
+++ b/web/src/app/(app)/api-keys/loading.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { LoadingPageWrapper } from "@/components/LoadingWrapper";
+import { usePathname } from "next/navigation";
+
+export default function Loading() {
+ const pathName = usePathname();
+ return ;
+}
diff --git a/web/src/app/(app)/machines/loading.tsx b/web/src/app/(app)/machines/loading.tsx
new file mode 100644
index 0000000..9ff4783
--- /dev/null
+++ b/web/src/app/(app)/machines/loading.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { LoadingPageWrapper } from "@/components/LoadingWrapper";
+import { usePathname } from "next/navigation";
+
+export default function Loading() {
+ const pathName = usePathname();
+ return ;
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/@deployment/page.tsx b/web/src/app/(app)/workflows/[workflow_id]/@deployment/page.tsx
new file mode 100644
index 0000000..34eb1f0
--- /dev/null
+++ b/web/src/app/(app)/workflows/[workflow_id]/@deployment/page.tsx
@@ -0,0 +1,25 @@
+import { LoadingWrapper } from "@/components/LoadingWrapper";
+import { DeploymentsTable } from "@/components/RunsTable";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export default async function Page({
+ params,
+}: {
+ params: { workflow_id: string };
+}) {
+ const workflow_id = params.workflow_id;
+
+ return (
+
+
+ Deployments
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/@runs/page.tsx b/web/src/app/(app)/workflows/[workflow_id]/@runs/page.tsx
new file mode 100644
index 0000000..2843009
--- /dev/null
+++ b/web/src/app/(app)/workflows/[workflow_id]/@runs/page.tsx
@@ -0,0 +1,27 @@
+import { LoadingWrapper } from "@/components/LoadingWrapper";
+import { RunsTable } from "@/components/RunsTable";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export default async function Page({
+ params,
+ searchParams,
+}: {
+ params: { workflow_id: string };
+ searchParams: { [key: string]: string | string[] | undefined };
+}) {
+ const workflow_id = params.workflow_id;
+
+ return (
+
+
+ Run
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/@workflow/loading.tsx b/web/src/app/(app)/workflows/[workflow_id]/@workflow/loading.tsx
new file mode 100644
index 0000000..175e6f8
--- /dev/null
+++ b/web/src/app/(app)/workflows/[workflow_id]/@workflow/loading.tsx
@@ -0,0 +1,11 @@
+import { LoadingPageWrapper } from "@/components/LoadingWrapper";
+import { Card } from "@/components/ui/card";
+
+export default function Loading() {
+ // You can add any UI inside Loading, including a Skeleton.
+ return (
+
+
+
+ );
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx b/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx
new file mode 100644
index 0000000..5ed3b8b
--- /dev/null
+++ b/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx
@@ -0,0 +1,56 @@
+import { MachinesWSMain } from "@/components/MachinesWS";
+import { VersionDetails } from "@/components/VersionDetails";
+import {
+ CopyWorkflowVersion,
+ CreateDeploymentButton,
+ MachineSelect,
+ RunWorkflowButton,
+ VersionSelect,
+ ViewWorkflowDetailsButton,
+} from "@/components/VersionSelect";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { getRelativeTime } from "@/lib/getRelativeTime";
+import { getMachines } from "@/server/curdMachine";
+import { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
+
+export default async function Page({
+ params,
+}: {
+ params: { workflow_id: string };
+}) {
+ const workflow_id = params.workflow_id;
+
+ const workflow = await findFirstTableWithVersion(workflow_id);
+ const machines = await getMachines();
+
+ return (
+
+
+ {workflow?.name}
+
+ {getRelativeTime(workflow?.updated_at)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/layout.tsx b/web/src/app/(app)/workflows/[workflow_id]/layout.tsx
new file mode 100644
index 0000000..7a2f06d
--- /dev/null
+++ b/web/src/app/(app)/workflows/[workflow_id]/layout.tsx
@@ -0,0 +1,22 @@
+export default async function Layout({
+ children,
+ deployment,
+ runs,
+ workflow,
+}: {
+ children: React.ReactNode;
+ deployment: React.ReactNode;
+ runs: React.ReactNode;
+ workflow: React.ReactNode;
+}) {
+ return (
+
+
+ {workflow}
+ {deployment}
+
+
+ {runs}
+
+ );
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/loading.tsx b/web/src/app/(app)/workflows/[workflow_id]/loading.tsx
new file mode 100644
index 0000000..372234d
--- /dev/null
+++ b/web/src/app/(app)/workflows/[workflow_id]/loading.tsx
@@ -0,0 +1,6 @@
+import { LoadingPageWrapper } from "@/components/LoadingWrapper";
+
+export default function Loading() {
+ // You can add any UI inside Loading, including a Skeleton.
+ return ;
+}
diff --git a/web/src/app/(app)/workflows/[workflow_id]/page.tsx b/web/src/app/(app)/workflows/[workflow_id]/page.tsx
deleted file mode 100644
index e56113c..0000000
--- a/web/src/app/(app)/workflows/[workflow_id]/page.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { DeploymentsTable, RunsTable } from "../../../../components/RunsTable";
-import { findFirstTableWithVersion } from "../../../../server/findFirstTableWithVersion";
-import { MachinesWSMain } from "@/components/MachinesWS";
-import { VersionDetails } from "@/components/VersionDetails";
-import {
- CopyWorkflowVersion,
- CreateDeploymentButton,
- MachineSelect,
- RunWorkflowButton,
- VersionSelect,
- ViewWorkflowDetailsButton,
-} from "@/components/VersionSelect";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { getRelativeTime } from "@/lib/getRelativeTime";
-import { getMachines } from "@/server/curdMachine";
-
-export default async function Page({
- params,
-}: {
- params: { workflow_id: string };
-}) {
- const workflow_id = params.workflow_id;
-
- const workflow = await findFirstTableWithVersion(workflow_id);
- const machines = await getMachines();
-
- return (
-
-
-
-
- {workflow?.name}
-
- {getRelativeTime(workflow?.updated_at)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deployments
-
-
-
-
-
-
-
-
-
-
- Run
-
-
-
-
-
-
-
- );
-}
diff --git a/web/src/app/(app)/workflows/loading.tsx b/web/src/app/(app)/workflows/loading.tsx
new file mode 100644
index 0000000..9ff4783
--- /dev/null
+++ b/web/src/app/(app)/workflows/loading.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { LoadingPageWrapper } from "@/components/LoadingWrapper";
+import { usePathname } from "next/navigation";
+
+export default function Loading() {
+ const pathName = usePathname();
+ return ;
+}
diff --git a/web/src/components/LoadingWrapper.tsx b/web/src/components/LoadingWrapper.tsx
new file mode 100644
index 0000000..84b8fd5
--- /dev/null
+++ b/web/src/components/LoadingWrapper.tsx
@@ -0,0 +1,37 @@
+import { LoadingIcon } from "@/components/LoadingIcon";
+import { cn } from "@/lib/utils";
+import { Suspense } from "react";
+
+export function LoadingWrapper(props: {
+ tag: string;
+ children?: React.ReactNode;
+}) {
+ return (
+
+ Fetching {props.tag}
+
+ }
+ >
+ {props.children}
+
+ );
+}
+
+export function LoadingPageWrapper(props: {
+ tag: string;
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ Fetching {props.tag}
+
+ );
+}
diff --git a/web/src/components/PaginationControl.tsx b/web/src/components/PaginationControl.tsx
new file mode 100644
index 0000000..19825dd
--- /dev/null
+++ b/web/src/components/PaginationControl.tsx
@@ -0,0 +1,42 @@
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from "@/components/ui/pagination";
+
+export function PaginationControl(props: {
+ totalPage: number;
+ currentPage: number;
+}) {
+ return (
+
+
+ 1
+ ? `?page=${props.currentPage - 1}`
+ : `?page=${props.currentPage}`
+ }
+ />
+
+ {props.currentPage}
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/components/RunsTable.tsx b/web/src/components/RunsTable.tsx
index 75d61a3..8ac592f 100644
--- a/web/src/components/RunsTable.tsx
+++ b/web/src/components/RunsTable.tsx
@@ -1,5 +1,9 @@
-import { findAllDeployments, findAllRuns } from "../server/findAllRuns";
+import {
+ findAllDeployments,
+ findAllRunsWithCounts,
+} from "../server/findAllRuns";
import { DeploymentDisplay } from "./DeploymentDisplay";
+import { PaginationControl } from "./PaginationControl";
import { RunDisplay } from "./RunDisplay";
import {
Table,
@@ -9,29 +13,51 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { parseAsInteger } from "next-usequerystate";
-export async function RunsTable(props: { workflow_id: string }) {
- const allRuns = await findAllRuns(props.workflow_id);
+const itemPerPage = 4;
+const pageParser = parseAsInteger.withDefault(1);
+
+export async function RunsTable(props: {
+ workflow_id: string;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) {
+ // await new Promise((resolve) => setTimeout(resolve, 5000));
+ const page = pageParser.parseServerSide(
+ props.searchParams?.page ?? undefined
+ );
+ const { allRuns, total } = await findAllRunsWithCounts({
+ workflow_id: props.workflow_id,
+ limit: itemPerPage,
+ offset: (page - 1) * itemPerPage,
+ });
return (
-
-
- A list of your recent runs.
-
-
- Number
- Machine
- Time
- Version
- Live Status
- Status
-
-
-
- {allRuns.map((run) => (
-
- ))}
-
-
+
+
+
+ {/* A list of your recent runs. */}
+
+
+ Number
+ Machine
+ Time
+ Version
+ Live Status
+ Status
+
+
+
+ {allRuns.map((run) => (
+
+ ))}
+
+
+
+
+
);
}
diff --git a/web/src/components/ui/pagination.tsx b/web/src/components/ui/pagination.tsx
new file mode 100644
index 0000000..5e5b375
--- /dev/null
+++ b/web/src/components/ui/pagination.tsx
@@ -0,0 +1,117 @@
+import type { ButtonProps } from "@/components/ui/button";
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
+import Link from "next/link";
+import * as React from "react";
+
+const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
+
+);
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+PaginationContent.displayName = "PaginationContent";
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+));
+PaginationItem.displayName = "PaginationItem";
+
+type PaginationLinkProps = {
+ isActive?: boolean;
+} & Pick
&
+ React.ComponentProps;
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = "icon",
+ ...props
+}: PaginationLinkProps) => (
+
+
+
+);
+PaginationLink.displayName = "PaginationLink";
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+);
+PaginationPrevious.displayName = "PaginationPrevious";
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+);
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More pages
+
+);
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+};
diff --git a/web/src/components/ui/skeleton.tsx b/web/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..01b8b6d
--- /dev/null
+++ b/web/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/web/src/server/findAllRuns.tsx b/web/src/server/findAllRuns.tsx
index d2c187a..a8398a1 100644
--- a/web/src/server/findAllRuns.tsx
+++ b/web/src/server/findAllRuns.tsx
@@ -1,14 +1,28 @@
+"use server";
+
import { db } from "@/db/db";
import { deploymentsTable, workflowRunsTable } from "@/db/schema";
-import { desc, eq, sql } from "drizzle-orm";
+import { count, desc, eq, sql } from "drizzle-orm";
-export async function findAllRuns(workflow_id: string) {
+type RunsSearchTypes = {
+ workflow_id: string;
+ limit: number;
+ offset: number;
+};
+
+export async function findAllRuns({
+ workflow_id,
+ limit = 10,
+ offset = 0,
+}: RunsSearchTypes) {
return await db.query.workflowRunsTable.findMany({
where: eq(workflowRunsTable.workflow_id, workflow_id),
orderBy: desc(workflowRunsTable.created_at),
- limit: 10,
+ offset: offset,
+ limit: limit,
extras: {
number: sql`row_number() over (order by created_at)`.as("number"),
+ total: sql`count(*) over ()`.as("total"),
},
with: {
machine: {
@@ -26,6 +40,20 @@ export async function findAllRuns(workflow_id: string) {
});
}
+export async function findAllRunsWithCounts(props: RunsSearchTypes) {
+ const a = await db
+ .select({
+ count: count(workflowRunsTable.id),
+ })
+ .from(workflowRunsTable)
+ .where(eq(workflowRunsTable.workflow_id, props.workflow_id));
+
+ return {
+ allRuns: await findAllRuns(props),
+ total: a[0].count,
+ };
+}
+
export async function findAllDeployments(workflow_id: string) {
return await db.query.deploymentsTable.findMany({
where: eq(deploymentsTable.workflow_id, workflow_id),