feat(web): paginated runs table, add loading page
This commit is contained in:
		
							parent
							
								
									744a222e26
								
							
						
					
					
						commit
						d63ab1924b
					
				
							
								
								
									
										
											BIN
										
									
								
								web/bun.lockb
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/bun.lockb
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							@ -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",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								web/src/app/(app)/api-keys/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/src/app/(app)/api-keys/loading.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
 | 
			
		||||
import { usePathname } from "next/navigation";
 | 
			
		||||
 | 
			
		||||
export default function Loading() {
 | 
			
		||||
  const pathName = usePathname();
 | 
			
		||||
  return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								web/src/app/(app)/machines/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/src/app/(app)/machines/loading.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
 | 
			
		||||
import { usePathname } from "next/navigation";
 | 
			
		||||
 | 
			
		||||
export default function Loading() {
 | 
			
		||||
  const pathName = usePathname();
 | 
			
		||||
  return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <Card className="w-full h-fit">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle>Deployments</CardTitle>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
 | 
			
		||||
      <CardContent>
 | 
			
		||||
        <LoadingWrapper tag="deployments">
 | 
			
		||||
          <DeploymentsTable workflow_id={workflow_id} />
 | 
			
		||||
        </LoadingWrapper>
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								web/src/app/(app)/workflows/[workflow_id]/@runs/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								web/src/app/(app)/workflows/[workflow_id]/@runs/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <Card className="w-full h-fit min-w-0">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle>Run</CardTitle>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
 | 
			
		||||
      <CardContent>
 | 
			
		||||
        <LoadingWrapper tag="runs">
 | 
			
		||||
          <RunsTable workflow_id={workflow_id} searchParams={searchParams} />
 | 
			
		||||
        </LoadingWrapper>
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <Card className="w-full h-fit">
 | 
			
		||||
      <LoadingPageWrapper tag="workflow details" />
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <Card className="w-full h-fit">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle>{workflow?.name}</CardTitle>
 | 
			
		||||
        <CardDescription suppressHydrationWarning={true}>
 | 
			
		||||
          {getRelativeTime(workflow?.updated_at)}
 | 
			
		||||
        </CardDescription>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
 | 
			
		||||
      <CardContent>
 | 
			
		||||
        <div className="flex gap-2 flex-wrap">
 | 
			
		||||
          <VersionSelect workflow={workflow} />
 | 
			
		||||
          <MachineSelect machines={machines} />
 | 
			
		||||
          <RunWorkflowButton workflow={workflow} machines={machines} />
 | 
			
		||||
          <CreateDeploymentButton workflow={workflow} machines={machines} />
 | 
			
		||||
          <CopyWorkflowVersion workflow={workflow} />
 | 
			
		||||
          <ViewWorkflowDetailsButton workflow={workflow} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <VersionDetails workflow={workflow} />
 | 
			
		||||
        <MachinesWSMain machines={machines} />
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								web/src/app/(app)/workflows/[workflow_id]/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/src/app/(app)/workflows/[workflow_id]/layout.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]">
 | 
			
		||||
      <div className="w-full flex gap-4 flex-col min-w-0">
 | 
			
		||||
        {workflow}
 | 
			
		||||
        {deployment}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {runs}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								web/src/app/(app)/workflows/[workflow_id]/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								web/src/app/(app)/workflows/[workflow_id]/loading.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
 | 
			
		||||
 | 
			
		||||
export default function Loading() {
 | 
			
		||||
  // You can add any UI inside Loading, including a Skeleton.
 | 
			
		||||
  return <LoadingPageWrapper tag="workflow" />;
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <div className="mt-4 w-full flex flex-col lg:flex-row gap-4 max-h-[calc(100dvh-100px)]">
 | 
			
		||||
      <div className="flex gap-4 flex-col">
 | 
			
		||||
        <Card className="w-full lg:w-fit lg:min-w-[600px] h-fit">
 | 
			
		||||
          <CardHeader>
 | 
			
		||||
            <CardTitle>{workflow?.name}</CardTitle>
 | 
			
		||||
            <CardDescription suppressHydrationWarning={true}>
 | 
			
		||||
              {getRelativeTime(workflow?.updated_at)}
 | 
			
		||||
            </CardDescription>
 | 
			
		||||
          </CardHeader>
 | 
			
		||||
 | 
			
		||||
          <CardContent>
 | 
			
		||||
            <div className="flex gap-2 flex-wrap">
 | 
			
		||||
              <VersionSelect workflow={workflow} />
 | 
			
		||||
              <MachineSelect machines={machines} />
 | 
			
		||||
              <RunWorkflowButton workflow={workflow} machines={machines} />
 | 
			
		||||
              <CreateDeploymentButton workflow={workflow} machines={machines} />
 | 
			
		||||
              <CopyWorkflowVersion workflow={workflow} />
 | 
			
		||||
              <ViewWorkflowDetailsButton workflow={workflow} />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <VersionDetails workflow={workflow} />
 | 
			
		||||
            <MachinesWSMain machines={machines} />
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        </Card>
 | 
			
		||||
        <Card className="w-full h-fit">
 | 
			
		||||
          <CardHeader>
 | 
			
		||||
            <CardTitle>Deployments</CardTitle>
 | 
			
		||||
          </CardHeader>
 | 
			
		||||
 | 
			
		||||
          <CardContent>
 | 
			
		||||
            <DeploymentsTable workflow_id={workflow_id} />
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        </Card>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Card className="w-full h-fit">
 | 
			
		||||
        <CardHeader>
 | 
			
		||||
          <CardTitle>Run</CardTitle>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
 | 
			
		||||
        <CardContent>
 | 
			
		||||
          <RunsTable workflow_id={workflow_id} />
 | 
			
		||||
        </CardContent>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								web/src/app/(app)/workflows/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/src/app/(app)/workflows/loading.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
 | 
			
		||||
import { usePathname } from "next/navigation";
 | 
			
		||||
 | 
			
		||||
export default function Loading() {
 | 
			
		||||
  const pathName = usePathname();
 | 
			
		||||
  return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								web/src/components/LoadingWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/components/LoadingWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <Suspense
 | 
			
		||||
      fallback={
 | 
			
		||||
        <div className="w-full py-4 flex justify-center items-center gap-2 text-sm">
 | 
			
		||||
          Fetching {props.tag} <LoadingIcon />
 | 
			
		||||
        </div>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </Suspense>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function LoadingPageWrapper(props: {
 | 
			
		||||
  tag: string;
 | 
			
		||||
  children?: React.ReactNode;
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "w-full py-4 flex justify-center items-center gap-2 text-sm",
 | 
			
		||||
        props.className
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      Fetching {props.tag} <LoadingIcon />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								web/src/components/PaginationControl.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/src/components/PaginationControl.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <Pagination>
 | 
			
		||||
      <PaginationContent>
 | 
			
		||||
        <PaginationPrevious
 | 
			
		||||
          href={
 | 
			
		||||
            props.currentPage > 1
 | 
			
		||||
              ? `?page=${props.currentPage - 1}`
 | 
			
		||||
              : `?page=${props.currentPage}`
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        <PaginationLink href="#" isActive>
 | 
			
		||||
          {props.currentPage}
 | 
			
		||||
        </PaginationLink>
 | 
			
		||||
        <PaginationItem>
 | 
			
		||||
          <PaginationEllipsis />
 | 
			
		||||
        </PaginationItem>
 | 
			
		||||
        <PaginationNext
 | 
			
		||||
          className="cursor-pointer"
 | 
			
		||||
          href={
 | 
			
		||||
            props.currentPage < props.totalPage
 | 
			
		||||
              ? `?page=${props.currentPage + 1}`
 | 
			
		||||
              : `?page=${props.totalPage}`
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </PaginationContent>
 | 
			
		||||
    </Pagination>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <div className="overflow-auto h-[400px] w-full">
 | 
			
		||||
      <Table className="">
 | 
			
		||||
        <TableCaption>A list of your recent runs.</TableCaption>
 | 
			
		||||
        <TableHeader className="bg-background top-0 sticky">
 | 
			
		||||
          <TableRow>
 | 
			
		||||
            <TableHead className="w-[100px]">Number</TableHead>
 | 
			
		||||
            <TableHead className="">Machine</TableHead>
 | 
			
		||||
            <TableHead className="">Time</TableHead>
 | 
			
		||||
            <TableHead className="w-[100px]">Version</TableHead>
 | 
			
		||||
            <TableHead className="">Live Status</TableHead>
 | 
			
		||||
            <TableHead className=" text-right">Status</TableHead>
 | 
			
		||||
          </TableRow>
 | 
			
		||||
        </TableHeader>
 | 
			
		||||
        <TableBody>
 | 
			
		||||
          {allRuns.map((run) => (
 | 
			
		||||
            <RunDisplay run={run} key={run.id} />
 | 
			
		||||
          ))}
 | 
			
		||||
        </TableBody>
 | 
			
		||||
      </Table>
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="overflow-auto h-[400px] w-full">
 | 
			
		||||
        <Table className="">
 | 
			
		||||
          {/* <TableCaption>A list of your recent runs.</TableCaption> */}
 | 
			
		||||
          <TableHeader className="bg-background top-0 sticky">
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableHead className="w-[100px]">Number</TableHead>
 | 
			
		||||
              <TableHead className="">Machine</TableHead>
 | 
			
		||||
              <TableHead className="">Time</TableHead>
 | 
			
		||||
              <TableHead className="w-[100px]">Version</TableHead>
 | 
			
		||||
              <TableHead className="">Live Status</TableHead>
 | 
			
		||||
              <TableHead className=" text-right">Status</TableHead>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHeader>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {allRuns.map((run) => (
 | 
			
		||||
              <RunDisplay run={run} key={run.id} />
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <PaginationControl
 | 
			
		||||
        totalPage={Math.ceil(total / itemPerPage)}
 | 
			
		||||
        currentPage={page}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										117
									
								
								web/src/components/ui/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								web/src/components/ui/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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">) => (
 | 
			
		||||
  <nav
 | 
			
		||||
    role="navigation"
 | 
			
		||||
    aria-label="pagination"
 | 
			
		||||
    className={cn("mx-auto flex w-full justify-center", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const PaginationContent = React.forwardRef<
 | 
			
		||||
  HTMLUListElement,
 | 
			
		||||
  React.ComponentProps<"ul">
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ul
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("flex flex-row items-center gap-1", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
PaginationContent.displayName = "PaginationContent";
 | 
			
		||||
 | 
			
		||||
const PaginationItem = React.forwardRef<
 | 
			
		||||
  HTMLLIElement,
 | 
			
		||||
  React.ComponentProps<"li">
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <li ref={ref} className={cn("", className)} {...props} />
 | 
			
		||||
));
 | 
			
		||||
PaginationItem.displayName = "PaginationItem";
 | 
			
		||||
 | 
			
		||||
type PaginationLinkProps = {
 | 
			
		||||
  isActive?: boolean;
 | 
			
		||||
} & Pick<ButtonProps, "size"> &
 | 
			
		||||
  React.ComponentProps<typeof Link>;
 | 
			
		||||
 | 
			
		||||
const PaginationLink = ({
 | 
			
		||||
  className,
 | 
			
		||||
  isActive,
 | 
			
		||||
  size = "icon",
 | 
			
		||||
  ...props
 | 
			
		||||
}: PaginationLinkProps) => (
 | 
			
		||||
  <PaginationItem>
 | 
			
		||||
    <Link
 | 
			
		||||
      aria-current={isActive ? "page" : undefined}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        buttonVariants({
 | 
			
		||||
          variant: isActive ? "outline" : "ghost",
 | 
			
		||||
          size,
 | 
			
		||||
        }),
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </PaginationItem>
 | 
			
		||||
);
 | 
			
		||||
PaginationLink.displayName = "PaginationLink";
 | 
			
		||||
 | 
			
		||||
const PaginationPrevious = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof PaginationLink>) => (
 | 
			
		||||
  <PaginationLink
 | 
			
		||||
    aria-label="Go to previous page"
 | 
			
		||||
    size="default"
 | 
			
		||||
    className={cn("gap-1 pl-2.5", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <ChevronLeft className="h-4 w-4" />
 | 
			
		||||
    <span>Previous</span>
 | 
			
		||||
  </PaginationLink>
 | 
			
		||||
);
 | 
			
		||||
PaginationPrevious.displayName = "PaginationPrevious";
 | 
			
		||||
 | 
			
		||||
const PaginationNext = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof PaginationLink>) => (
 | 
			
		||||
  <PaginationLink
 | 
			
		||||
    aria-label="Go to next page"
 | 
			
		||||
    size="default"
 | 
			
		||||
    className={cn("gap-1 pr-2.5", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <span>Next</span>
 | 
			
		||||
    <ChevronRight className="h-4 w-4" />
 | 
			
		||||
  </PaginationLink>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const PaginationEllipsis = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<"span">) => (
 | 
			
		||||
  <span
 | 
			
		||||
    aria-hidden
 | 
			
		||||
    className={cn("flex h-9 w-9 items-center justify-center", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <MoreHorizontal className="h-4 w-4" />
 | 
			
		||||
    <span className="sr-only">More pages</span>
 | 
			
		||||
  </span>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Pagination,
 | 
			
		||||
  PaginationContent,
 | 
			
		||||
  PaginationEllipsis,
 | 
			
		||||
  PaginationItem,
 | 
			
		||||
  PaginationLink,
 | 
			
		||||
  PaginationNext,
 | 
			
		||||
  PaginationPrevious,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										15
									
								
								web/src/components/ui/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/src/components/ui/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Skeleton({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={cn("animate-pulse rounded-md bg-muted", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Skeleton }
 | 
			
		||||
@ -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<number>`row_number() over (order by created_at)`.as("number"),
 | 
			
		||||
      total: sql<number>`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),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user