[] = [
+ {
+ accessorKey: "name",
+ header: "Name",
+ cell: ({ row }) => row.original.name,
+ },
+ {
+ accessorKey: "status",
+ header: "Status",
+ cell: ({ row }) => {
+ return row.original.status;
+ },
+ },
+ {
+ accessorKey: "tags",
+ header: "Tags",
+ cell: ({ row }) => (
+
+ {row.original.tags?.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+ ),
+ },
+ {
+ accessorKey: "schedules",
+ header: "Schedules",
+ cell: ({ row }) => (
+
+ {row.original.schedules?.map((schedule, index) => {
+ if (
+ schedule.schedule &&
+ typeof schedule.schedule === "object" &&
+ "cron" in schedule.schedule
+ ) {
+ const cronExpression = schedule.schedule.cron;
+ return (
+
+ Cron: {cronExpression}
+
+ );
+ } else if (
+ schedule.schedule &&
+ typeof schedule.schedule === "object" &&
+ "interval" in schedule.schedule
+ ) {
+ return (
+
+ Interval: {schedule.schedule.interval} seconds
+
+ );
+ } else if (
+ schedule.schedule &&
+ typeof schedule.schedule === "object" &&
+ "rrule" in schedule.schedule
+ ) {
+ return (
+
+ RRule: {schedule.schedule.rrule}
+
+ );
+ } else {
+ return (
+
+ {JSON.stringify(schedule.schedule)}
+
+ );
+ }
+ })}
+
+ ),
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ if (!row.original.id) return null;
+
+ return (
+
+
+
+
+
+ Quick run
+ Custom run
+
+ void navigator.clipboard.writeText(row.original.id as string)
+ }
+ >
+ Copy ID
+
+ Edit
+ Delete
+ Duplicate
+ Manage Access
+ Add to incident
+
+
+ );
+ },
+ },
+];
diff --git a/ui-v2/src/components/flows/detail/index.tsx b/ui-v2/src/components/flows/detail/index.tsx
new file mode 100644
index 000000000000..a5581850f2d7
--- /dev/null
+++ b/ui-v2/src/components/flows/detail/index.tsx
@@ -0,0 +1,186 @@
+import { components } from "@/api/prefect";
+import { DataTable } from "@/components/ui/data-table";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { useNavigate } from "@tanstack/react-router";
+import { columns as deploymentColumns } from "./deployment-columns";
+import {
+ getFlowMetadata,
+ columns as metadataColumns,
+} from "./metadata-columns";
+import { columns as flowRunColumns } from "./runs-columns";
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Input } from "@/components/ui/input";
+import {
+ getCoreRowModel,
+ getPaginationRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import { ChevronDownIcon, SearchIcon } from "lucide-react";
+
+const SearchComponent = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ void navigate({
+ to: ".",
+ search: (prev) => ({
+ ...prev,
+ "runs.flowRuns.nameLike": e.target.value,
+ }),
+ })
+ }
+ />
+
+
+ );
+};
+
+const SortComponent = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+
+ void navigate({
+ to: ".",
+ search: (prev) => ({ ...prev, "runs.sort": "START_TIME_DESC" }),
+ })
+ }
+ >
+ Newest
+
+
+ void navigate({
+ to: ".",
+ search: (prev) => ({ ...prev, "runs.sort": "START_TIME_ASC" }),
+ })
+ }
+ >
+ Oldest
+
+
+
+ );
+};
+
+export default function FlowDetail({
+ flow,
+ flowRuns,
+ activity,
+ deployments,
+ tab = "runs",
+}: {
+ flow: components["schemas"]["Flow"];
+ flowRuns: components["schemas"]["FlowRun"][];
+ activity: components["schemas"]["FlowRun"][];
+ deployments: components["schemas"]["DeploymentResponse"][];
+ tab: "runs" | "deployments" | "details";
+}): React.ReactElement {
+ const navigate = useNavigate();
+ console.log(activity);
+
+ const flowRunTable = useReactTable({
+ data: flowRuns,
+ columns: flowRunColumns,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ initialState: {
+ pagination: {
+ pageIndex: 0,
+ pageSize: 10,
+ },
+ },
+ });
+
+ const deploymentsTable = useReactTable({
+ data: deployments,
+ columns: deploymentColumns,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ initialState: {
+ pagination: {
+ pageIndex: 0,
+ pageSize: 10,
+ },
+ },
+ });
+
+ const metadataTable = useReactTable({
+ columns: metadataColumns,
+ data: getFlowMetadata(flow),
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ onPaginationChange: (pagination) => {
+ console.log(pagination);
+ return pagination;
+ },
+ initialState: {
+ pagination: {
+ pageIndex: 0,
+ pageSize: 10,
+ },
+ },
+ });
+
+ return (
+
+
+ void navigate({
+ to: ".",
+ search: (prev) => ({
+ ...prev,
+ tab: value as "runs" | "deployments" | "details",
+ }),
+ })
+ }
+ >
+
+ Runs
+ Deployments
+ Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui-v2/src/components/flows/detail/metadata-columns.tsx b/ui-v2/src/components/flows/detail/metadata-columns.tsx
new file mode 100644
index 000000000000..4474fad98917
--- /dev/null
+++ b/ui-v2/src/components/flows/detail/metadata-columns.tsx
@@ -0,0 +1,28 @@
+import { components } from "@/api/prefect";
+import { ColumnDef } from "@tanstack/react-table";
+
+type Flow = components["schemas"]["Flow"];
+type FlowMetadata = { attribute: string; value: string | string[] | null };
+
+export const columns: ColumnDef[] = [
+ {
+ accessorKey: "attribute",
+ header: "Attribute",
+ cell: ({ row }) => (
+ {row.original.attribute}
+ ),
+ },
+ {
+ accessorKey: "value",
+ header: "Value",
+ cell: ({ row }) => row.original.value,
+ },
+];
+
+export const getFlowMetadata = (flow: Flow): FlowMetadata[] => [
+ { attribute: "ID", value: flow.id || null },
+ { attribute: "Name", value: flow.name },
+ { attribute: "Created", value: flow.created || null },
+ { attribute: "Updated", value: flow.updated || null },
+ { attribute: "Tags", value: flow.tags || [] },
+];
diff --git a/ui-v2/src/components/flows/detail/runs-columns.tsx b/ui-v2/src/components/flows/detail/runs-columns.tsx
new file mode 100644
index 000000000000..6f933ef5d4a0
--- /dev/null
+++ b/ui-v2/src/components/flows/detail/runs-columns.tsx
@@ -0,0 +1,81 @@
+import { components } from "@/api/prefect";
+import { ColumnDef } from "@tanstack/react-table";
+import { format, parseISO } from "date-fns";
+import { DeploymentCell, WorkPoolCell } from "./cells";
+
+type FlowRun = components["schemas"]["FlowRun"];
+
+export const columns: ColumnDef[] = [
+ {
+ accessorKey: "created",
+ header: "Time",
+ cell: ({ row }) => (
+
+ {row.original.created &&
+ format(parseISO(row.original.created), "MMM dd HH:mm:ss OOOO")}
+
+ ),
+ },
+ {
+ accessorKey: "state",
+ header: "State",
+ cell: ({ row }) => (
+
+ {row.original.state?.name}
+
+ ),
+ },
+ {
+ accessorKey: "name",
+ header: "Name",
+ cell: ({ row }) => row.original.name,
+ },
+ {
+ accessorKey: "deployment",
+ header: "Deployment",
+ cell: ({ row }) => ,
+ },
+ {
+ accessorKey: "work_pool",
+ header: "Work Pool",
+ cell: ({ row }) => ,
+ },
+ {
+ accessorKey: "work_queue",
+ header: "Work Queue",
+ cell: ({ row }) => row.original.work_queue_name,
+ },
+ {
+ accessorKey: "tags",
+ header: "Tags",
+ cell: ({ row }) =>
+ row.original.tags?.map((tag, index) => (
+
+ {tag}
+
+ )),
+ },
+ {
+ accessorKey: "duration",
+ header: "Duration",
+ cell: ({ row }) => (
+
+ {row.original.estimated_run_time
+ ? `${row.original.estimated_run_time}s`
+ : "-"}
+
+ ),
+ },
+];
diff --git a/ui-v2/src/components/flows/queries.tsx b/ui-v2/src/components/flows/queries.tsx
new file mode 100644
index 000000000000..69918675f982
--- /dev/null
+++ b/ui-v2/src/components/flows/queries.tsx
@@ -0,0 +1,309 @@
+import { components } from "@/api/prefect";
+import { QueryService } from "@/api/service";
+import {
+ MutationFunction,
+ QueryFunction,
+ QueryKey,
+ QueryObserverOptions,
+} from "@tanstack/react-query";
+import { format } from "date-fns";
+
+export const flowQueryParams = (
+ flowId: string,
+ queryParams: Partial = {},
+): {
+ queryKey: QueryKey;
+ queryFn: QueryFunction;
+} => ({
+ ...queryParams,
+ queryKey: ["flows", flowId] as const,
+ queryFn: async (): Promise => {
+ const response = await QueryService.GET("/flows/{id}", {
+ params: { path: { id: flowId } },
+ }).then((response) => response.data);
+ return response as components["schemas"]["Flow"];
+ },
+});
+
+export const flowRunsQueryParams = (
+ id: string,
+ body: components["schemas"]["Body_read_flow_runs_flow_runs_filter_post"],
+ queryParams: Partial = {},
+): {
+ queryKey: readonly ["flowRun", string];
+ queryFn: () => Promise;
+} => ({
+ ...queryParams,
+ queryKey: ["flowRun", JSON.stringify({ flowId: id, ...body })] as const,
+ queryFn: async () => {
+ const response = await QueryService.POST("/flow_runs/filter", {
+ body: {
+ ...body,
+ flows: { ...body.flows, operator: "and_" as const, id: { any_: [id] } },
+ },
+ }).then((response) => response.data);
+ return response as components["schemas"]["FlowRunResponse"][];
+ },
+});
+
+export const getLatestFlowRunsQueryParams = (
+ id: string,
+ n: number,
+ queryParams: Partial = {},
+): {
+ queryKey: readonly ["flowRun", string];
+ queryFn: () => Promise;
+} => ({
+ ...queryParams,
+ queryKey: [
+ "flowRun",
+ JSON.stringify({
+ flowId: id,
+ offset: 0,
+ limit: n,
+ sort: "START_TIME_DESC",
+ }),
+ ] as const,
+ queryFn: async () => {
+ const response = await QueryService.POST("/flow_runs/filter", {
+ body: {
+ flows: { operator: "and_" as const, id: { any_: [id] } },
+ flow_runs: {
+ operator: "and_" as const,
+ start_time: {
+ before_: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
+ is_null_: false,
+ },
+ },
+ offset: 0,
+ limit: n,
+ sort: "START_TIME_DESC",
+ },
+ }).then((response) => response.data);
+ return response as components["schemas"]["FlowRunResponse"][];
+ },
+});
+
+export const getNextFlowRunsQueryParams = (
+ id: string,
+ n: number,
+ queryParams: Partial = {},
+): {
+ queryKey: readonly ["flowRun", string];
+ queryFn: () => Promise;
+} => ({
+ ...queryParams,
+ queryKey: [
+ "flowRun",
+ JSON.stringify({
+ flowId: id,
+ offset: 0,
+ limit: n,
+ sort: "EXPECTED_START_TIME_ASC",
+ }),
+ ] as const,
+ queryFn: async () => {
+ const response = await QueryService.POST("/flow_runs/filter", {
+ body: {
+ flows: { operator: "and_" as const, id: { any_: [id] } },
+ flow_runs: {
+ operator: "and_" as const,
+ expected_start_time: {
+ after_: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
+ },
+ },
+ offset: 0,
+ limit: n,
+ sort: "EXPECTED_START_TIME_ASC",
+ },
+ }).then((response) => response.data);
+ return response as components["schemas"]["FlowRunResponse"][];
+ },
+});
+
+export const flowRunsCountQueryParams = (
+ id: string,
+ body?: components["schemas"]["Body_count_flow_runs_flow_runs_count_post"],
+ queryParams: Partial = {},
+): {
+ queryKey: readonly ["flowRunCount", string];
+ queryFn: () => Promise;
+} => ({
+ ...queryParams,
+ queryKey: ["flowRunCount", JSON.stringify({ flowId: id, ...body })] as const,
+ queryFn: async () => {
+ const response = await QueryService.POST("/flow_runs/count", {
+ body: {
+ ...body,
+ flows: {
+ ...body?.flows,
+ operator: "and_" as const,
+ id: { any_: [id] },
+ },
+ flow_runs: {
+ operator: "and_" as const,
+ expected_start_time: {
+ before_: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
+ },
+ ...body?.flow_runs,
+ },
+ },
+ }).then((response) => response.data);
+ return response as number;
+ },
+});
+
+export const deploymentsQueryParams = (
+ id: string,
+ body: components["schemas"]["Body_read_deployments_deployments_filter_post"],
+ queryParams: Partial = {},
+): {
+ queryKey: readonly ["deployments", string];
+ queryFn: () => Promise;
+} => ({
+ ...queryParams,
+ queryKey: ["deployments", JSON.stringify({ ...body, flowId: id })] as const,
+ queryFn: async () => {
+ const response = await QueryService.POST("/deployments/filter", {
+ body: {
+ ...body,
+ flows: {
+ ...body?.flows,
+ operator: "and_" as const,
+ id: { any_: [id] },
+ },
+ },
+ }).then((response) => response.data);
+ return response as components["schemas"]["DeploymentResponse"][];
+ },
+});
+
+export const deploymentsCountQueryParams = (
+ id: string,
+ queryParams: Partial = {},
+): {
+ queryKey: readonly ["deploymentsCount", string];
+ queryFn: () => Promise;
+} => ({
+ ...queryParams,
+ queryKey: ["deploymentsCount", JSON.stringify({ flowId: id })] as const,
+ queryFn: async () => {
+ const response = await QueryService.POST("/deployments/count", {
+ body: { flows: { operator: "and_" as const, id: { any_: [id] } } },
+ }).then((response) => response.data);
+ return response as number;
+ },
+});
+
+export const deleteFlowMutation = (
+ id: string,
+): {
+ mutationFn: MutationFunction;
+} => ({
+ mutationFn: async () => {
+ await QueryService.DELETE("/flows/{id}", { params: { path: { id } } });
+ },
+});
+
+// Define the Flow class
+export class FlowQuery {
+ private flowId: string;
+
+ /**
+ * Initializes a new instance of the Flow class.
+ * @param flowId - The ID of the flow.
+ */
+ constructor(flowId: string) {
+ this.flowId = flowId;
+ }
+
+ public getQueryParams(queryParams: Partial = {}): {
+ queryKey: QueryKey;
+ queryFn: QueryFunction;
+ } {
+ return flowQueryParams(this.flowId, queryParams);
+ }
+
+ public getFlowRunsQueryParams(
+ body: components["schemas"]["Body_read_flow_runs_flow_runs_filter_post"],
+ queryParams: Partial = {},
+ ): {
+ queryKey: readonly ["flowRun", string];
+ queryFn: () => Promise;
+ } {
+ return flowRunsQueryParams(this.flowId, body, queryParams);
+ }
+
+ public getLatestFlowRunsQueryParams(
+ n: number,
+ queryParams: Partial = {},
+ ): {
+ queryKey: readonly ["flowRun", string];
+ queryFn: () => Promise;
+ } {
+ return flowRunsQueryParams(
+ this.flowId,
+ { offset: 0, limit: n, sort: "START_TIME_DESC" },
+ queryParams,
+ );
+ }
+
+ public getNextFlowRunsQueryParams(
+ n: number,
+ queryParams: Partial = {},
+ ): {
+ queryKey: readonly ["flowRun", string];
+ queryFn: () => Promise;
+ } {
+ return flowRunsQueryParams(
+ this.flowId,
+ {
+ offset: 0,
+ limit: n,
+ sort: "EXPECTED_START_TIME_ASC",
+ flow_runs: {
+ operator: "and_",
+ expected_start_time: {
+ after_: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
+ },
+ },
+ },
+ queryParams,
+ );
+ }
+
+ public getFlowRunsCountQueryParams(
+ body?: components["schemas"]["Body_count_flow_runs_flow_runs_count_post"],
+ queryParams: Partial = {},
+ ): {
+ queryKey: readonly ["flowRunCount", string];
+ queryFn: () => Promise;
+ } {
+ return flowRunsCountQueryParams(this.flowId, body, queryParams);
+ }
+
+ public getDeploymentsQueryParams(
+ body: components["schemas"]["Body_read_deployments_deployments_filter_post"],
+ queryParams: Partial = {},
+ ): {
+ queryKey: readonly ["deployments", string];
+ queryFn: () => Promise;
+ } {
+ return deploymentsQueryParams(this.flowId, body, queryParams);
+ }
+
+ public getDeploymentsCountQueryParams(
+ queryParams: Partial = {},
+ ): {
+ queryKey: readonly ["deploymentsCount", string];
+ queryFn: () => Promise;
+ } {
+ return deploymentsCountQueryParams(this.flowId, queryParams);
+ }
+
+ public getDeleteFlowMutation(): {
+ mutationFn: MutationFunction;
+ } {
+ return deleteFlowMutation(this.flowId);
+ }
+}
diff --git a/ui-v2/src/components/layouts/MainLayout.tsx b/ui-v2/src/components/layouts/MainLayout.tsx
new file mode 100644
index 000000000000..ad3943ea613d
--- /dev/null
+++ b/ui-v2/src/components/layouts/MainLayout.tsx
@@ -0,0 +1,31 @@
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { NavItem } from "@/components/ui/sidebar";
+import { Workflow } from "lucide-react";
+
+export function MainLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
diff --git a/ui-v2/src/components/ui/breadcrumb.tsx b/ui-v2/src/components/ui/breadcrumb.tsx
new file mode 100644
index 000000000000..033e3c54517b
--- /dev/null
+++ b/ui-v2/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,134 @@
+import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
+import { Slot } from "@radix-ui/react-slot";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = "Breadcrumb";
+
+type BreadcrumbListProps = React.ComponentPropsWithoutRef<"ol"> & {
+ className?: string;
+};
+
+const BreadcrumbList = React.forwardRef(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+BreadcrumbList.displayName = "BreadcrumbList";
+
+type BreadcrumbItemProps = React.ComponentPropsWithoutRef<"li"> & {
+ className?: string;
+};
+
+const BreadcrumbItem = React.forwardRef(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+BreadcrumbItem.displayName = "BreadcrumbItem";
+
+type BreadcrumbLinkProps = React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean;
+ className?: string;
+};
+
+const BreadcrumbLink = React.forwardRef(
+ ({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+
+ );
+ },
+);
+BreadcrumbLink.displayName = "BreadcrumbLink";
+
+type BreadcrumbPageProps = React.ComponentPropsWithoutRef<"span"> & {
+ className?: string;
+};
+
+const BreadcrumbPage = React.forwardRef(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+BreadcrumbPage.displayName = "BreadcrumbPage";
+
+type BreadcrumbSeparatorProps = React.ComponentPropsWithoutRef<"li"> & {
+ className?: string;
+};
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: BreadcrumbSeparatorProps) => (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
+
+type BreadcrumbEllipsisProps = React.ComponentPropsWithoutRef<"span"> & {
+ className?: string;
+};
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: BreadcrumbEllipsisProps) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/ui-v2/src/components/ui/button/button.tsx b/ui-v2/src/components/ui/button/button.tsx
new file mode 100644
index 000000000000..2cf8ca1fe252
--- /dev/null
+++ b/ui-v2/src/components/ui/button/button.tsx
@@ -0,0 +1,28 @@
+import { Slot } from "@radix-ui/react-slot";
+import { type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "./styles";
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button };
diff --git a/ui-v2/src/components/ui/button/index.ts b/ui-v2/src/components/ui/button/index.ts
new file mode 100644
index 000000000000..cfd75ca081e3
--- /dev/null
+++ b/ui-v2/src/components/ui/button/index.ts
@@ -0,0 +1,2 @@
+export * from "./button";
+export * from "./styles";
diff --git a/ui-v2/src/components/ui/button/styles.ts b/ui-v2/src/components/ui/button/styles.ts
new file mode 100644
index 000000000000..b7cd7db9e761
--- /dev/null
+++ b/ui-v2/src/components/ui/button/styles.ts
@@ -0,0 +1,31 @@
+import { cva } from "class-variance-authority";
+
+export const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
diff --git a/ui-v2/src/components/ui/calendar.tsx b/ui-v2/src/components/ui/calendar.tsx
new file mode 100644
index 000000000000..9fb2c4e8ed95
--- /dev/null
+++ b/ui-v2/src/components/ui/calendar.tsx
@@ -0,0 +1,75 @@
+import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
+import * as React from "react";
+import { DayPicker } from "react-day-picker";
+
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+type CalendarProps = React.ComponentProps & {
+ className?: string;
+ classNames?: Record;
+ showOutsideDays?: boolean;
+ mode?: "default" | "single" | "multiple" | "range" | undefined;
+};
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md",
+ ),
+ day: cn(
+ buttonVariants({ variant: "ghost" }),
+ "h-8 w-8 p-0 font-normal aria-selected:opacity-100",
+ ),
+ day_range_start: "day-range-start",
+ day_range_end: "day-range-end",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: () => ,
+ IconRight: () => ,
+ }}
+ {...props}
+ />
+ );
+}
+Calendar.displayName = "Calendar";
+
+export { Calendar };
diff --git a/ui-v2/src/components/ui/card.tsx b/ui-v2/src/components/ui/card.tsx
new file mode 100644
index 000000000000..78cd0c348b24
--- /dev/null
+++ b/ui-v2/src/components/ui/card.tsx
@@ -0,0 +1,83 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/ui-v2/src/components/ui/chart.tsx b/ui-v2/src/components/ui/chart.tsx
new file mode 100644
index 000000000000..fc33fc2fa252
--- /dev/null
+++ b/ui-v2/src/components/ui/chart.tsx
@@ -0,0 +1,369 @@
+/* eslint-disable */
+// This file was generated by shadcn-ui, but raises a lot of eslint errors.
+// TODO: Fix eslint errors.
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+type ChartContainerProps = React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ className?: string;
+ id?: string;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"];
+};
+
+const ChartContainer = React.forwardRef(
+ ({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+ },
+);
+ChartContainer.displayName = "Chart";
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color,
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+