💄 Gave a relooking to the blueprint table

This commit is contained in:
Fred KISSIE
2025-10-25 03:06:45 +02:00
parent 2cc4ad9c30
commit dd052fa1af
6 changed files with 180 additions and 198 deletions

View File

@@ -1164,6 +1164,7 @@
"blueprintErrorCreateDescription": "An error occurred when applying the blueprint", "blueprintErrorCreateDescription": "An error occurred when applying the blueprint",
"blueprintErrorCreate": "Error creating blueprint", "blueprintErrorCreate": "Error creating blueprint",
"searchBlueprintProgress": "Search blueprints...", "searchBlueprintProgress": "Search blueprints...",
"appliedAt": "Applied at",
"source": "Source", "source": "Source",
"contents": "Contents", "contents": "Contents",
"enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocket": "Enable Docker Blueprint",

View File

@@ -9,6 +9,7 @@ import logger from "@server/logger";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { warn } from "console"; import { warn } from "console";
import { BlueprintData } from "./types";
const listBluePrintsParamsSchema = z const listBluePrintsParamsSchema = z
.object({ .object({
@@ -50,16 +51,18 @@ async function queryBlueprints(orgId: string, limit: number, offset: number) {
return res; return res;
} }
type BlueprintData = Omit<
Awaited<ReturnType<typeof queryBlueprints>>[number],
"source" | "createdAt"
> & {
source: "API" | "UI" | "NEWT";
createdAt: Date;
};
export type ListBlueprintsResponse = { export type ListBlueprintsResponse = {
blueprints: NonNullable<BlueprintData[]>; blueprints: NonNullable<
Pick<
BlueprintData,
| "blueprintId"
| "name"
| "source"
| "succeeded"
| "orgId"
| "createdAt"
>[]
>;
pagination: { total: number; limit: number; offset: number }; pagination: { total: number; limit: number; offset: number };
}; };

View File

@@ -0,0 +1,8 @@
import type { Blueprint } from "@server/db";
export type BlueprintSource = "API" | "UI" | "NEWT";
export type BlueprintData = Omit<Blueprint, "source" | "createdAt"> & {
source: BlueprintSource;
createdAt: Date;
};

View File

@@ -19,7 +19,7 @@ type BluePrintsPageProps = {
}; };
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Blueprint" title: "Blueprints"
}; };
export default async function BluePrintsPage(props: BluePrintsPageProps) { export default async function BluePrintsPage(props: BluePrintsPageProps) {

View File

@@ -3,7 +3,15 @@
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { DomainsDataTable } from "@app/components/DomainsDataTable"; import { DomainsDataTable } from "@app/components/DomainsDataTable";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import {
ArrowRight,
ArrowUpDown,
Globe,
LucideIcon,
MoreHorizontal,
Terminal,
Webhook
} from "lucide-react";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
@@ -16,11 +24,16 @@ import CreateDomainForm from "@app/components/CreateDomainForm";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { DataTable } from "./ui/data-table"; import { DataTable } from "./ui/data-table";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "./ui/dropdown-menu";
import Link from "next/link"; import Link from "next/link";
import { ListBlueprintsResponse } from "@server/routers/blueprints"; import { ListBlueprintsResponse } from "@server/routers/blueprints";
export type BlueprintRow = ListBlueprintsResponse['blueprints'][number] export type BlueprintRow = ListBlueprintsResponse["blueprints"][number];
type Props = { type Props = {
blueprints: BlueprintRow[]; blueprints: BlueprintRow[];
@@ -28,14 +41,38 @@ type Props = {
}; };
export default function BlueprintsTable({ blueprints, orgId }: Props) { export default function BlueprintsTable({ blueprints, orgId }: Props) {
const t = useTranslations(); const t = useTranslations();
const [isRefreshing, startTransition] = useTransition() const [isRefreshing, startTransition] = useTransition();
const router = useRouter() const router = useRouter();
const columns: ColumnDef<BlueprintRow>[] = [ const columns: ColumnDef<BlueprintRow>[] = [
{
accessorKey: "createdAt",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("appliedAt")}
<ArrowUpDown className="ml-2 size-4" />
</Button>
);
},
cell: ({ row }) => {
return (
<time
className="text-muted-foreground"
dateTime={row.original.createdAt.toString()}
>
{new Date(row.original.createdAt).toLocaleString()}
</time>
);
}
},
{ {
accessorKey: "name", accessorKey: "name",
header: ({ column }) => { header: ({ column }) => {
@@ -47,11 +84,12 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
} }
> >
{t("name")} {t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 size-4" />
</Button> </Button>
); );
} }
}, },
{ {
accessorKey: "source", accessorKey: "source",
header: ({ column }) => { header: ({ column }) => {
@@ -63,129 +101,118 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
} }
> >
{t("source")} {t("source")}
<ArrowUpDown className="ml-2 size-4" />
</Button>
);
},
cell: ({ row }) => {
const originalRow = row.original;
switch (originalRow.source) {
case "API": {
return (
<Badge variant="secondary">
<span className="inline-flex items-center gap-1 ">
API
<Webhook className="size-4 flex-none" />
</span>
</Badge>
);
}
case "NEWT": {
return (
<Badge variant="secondary">
<span className="inline-flex items-center gap-1 ">
Newt CLI
<Terminal className="size-4 flex-none" />
</span>
</Badge>
);
}
case "UI": {
return (
<Badge variant="secondary">
<span className="inline-flex items-center gap-1 ">
Dashboard{" "}
<Globe className="size-4 flex-none" />
</span>
</Badge>
);
}
}
}
},
{
accessorKey: "succeeded",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("status")}
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
}, },
// cell: ({ row }) => { cell: ({ row }) => {
// const originalRow = row.original; const { succeeded } = row.original;
// if ( if (succeeded) {
// originalRow.type == "newt" || return <Badge variant="green">{t("success")}</Badge>;
// originalRow.type == "wireguard" } else {
// ) { return (
// if (originalRow.online) { <Badge variant="red">
// return ( {t("failed", { fallback: "Failed" })}
// <span className="text-green-500 flex items-center space-x-2"> </Badge>
// <div className="w-2 h-2 bg-green-500 rounded-full"></div> );
// <span>{t("online")}</span> }
// </span> }
// );
// } else {
// return (
// <span className="text-neutral-500 flex items-center space-x-2">
// <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
// <span>{t("offline")}</span>
// </span>
// );
// }
// } else {
// return <span>-</span>;
// }
// }
}, },
// { {
// accessorKey: "nice", id: "actions",
// header: ({ column }) => { header: ({ column }) => {
// return ( return (
// <Button <span className="text-muted-foreground p-3">
// variant="ghost" {t("actions")}
// onClick={() => </span>
// column.toggleSorting(column.getIsSorted() === "asc") );
// } },
// className="hidden md:flex whitespace-nowrap" cell: ({ row }) => {
// > const domain = row.original;
// {t("site")}
// <ArrowUpDown className="ml-2 h-4 w-4" />
// </Button>
// );
// },
// // cell: ({ row }) => {
// // return (
// // <div className="hidden md:block whitespace-nowrap">
// // {row.original.nice}
// // </div>
// // );
// // }
// },
// {
// id: "actions",
// cell: ({ row }) => {
// const siteRow = row.original;
// return (
// <div className="flex items-center justify-end gap-2">
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" className="h-8 w-8 p-0">
// <span className="sr-only">Open menu</span>
// <MoreHorizontal className="h-4 w-4" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// <Link
// className="block w-full"
// href="#"
// // href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
// >
// <DropdownMenuItem>
// {t("viewSettings")}
// </DropdownMenuItem>
// </Link>
// <DropdownMenuItem
// onClick={() => {
// // setSelectedSite(siteRow);
// // setIsDeleteModalOpen(true);
// }}
// >
// <span className="text-red-500">
// {t("delete")}
// </span>
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// <Link return (
// href="#" <Button variant="outline" className="items-center" asChild>
// // href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`} <Link href={`#`}>
// > View details{" "}
// <Button variant={"secondary"} size="sm"> <ArrowRight className="size-4 flex-none" />
// {t("edit")} </Link>
// <ArrowRight className="ml-2 w-4 h-4" /> </Button>
// </Button> );
// </Link> }
// </div> }
// );
// }
// }
]; ];
return <DataTable return (
columns={columns} <DataTable
data={blueprints} columns={columns}
persistPageSize="blueprint-table" data={blueprints}
title={t('blueprints')} persistPageSize="blueprint-table"
searchPlaceholder={t('searchBlueprintProgress')} title={t("blueprints")}
searchColumn="name" searchPlaceholder={t("searchBlueprintProgress")}
onAdd={() => { searchColumn="name"
router.push(`/${orgId}/settings/blueprints/create`); onAdd={() => {
}} router.push(`/${orgId}/settings/blueprints/create`);
addButtonText={t('blueprintAdd')} }}
onRefresh={() => { addButtonText={t("blueprintAdd")}
startTransition(() => router.refresh()) onRefresh={() => {
}} startTransition(() => router.refresh());
isRefreshing={isRefreshing} }}
defaultSort={{ isRefreshing={isRefreshing}
id: "name", defaultSort={{
desc: false id: "name",
}} desc: false
/> }}
} />
);
}

View File

@@ -33,6 +33,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import type { CreateBlueprintResponse } from "@server/routers/blueprints"; import type { CreateBlueprintResponse } from "@server/routers/blueprints";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
export type CreateBlueprintFormProps = { export type CreateBlueprintFormProps = {
orgId: string; orgId: string;
@@ -45,6 +46,7 @@ export default function CreateBlueprintForm({
const { env } = useEnvContext(); const { env } = useEnvContext();
const api = createApiClient({ env }); const api = createApiClient({ env });
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
const router = useRouter();
const form = useForm({ const form = useForm({
resolver: zodResolver( resolver: zodResolver(
@@ -87,7 +89,6 @@ export default function CreateBlueprintForm({
console.log({ console.log({
isValid, isValid,
payload payload
// json: parse(data.contents)
}); });
const res = await api const res = await api
.put< .put<
@@ -107,68 +108,10 @@ export default function CreateBlueprintForm({
if (res && res.status === 201) { if (res && res.status === 201) {
toast({ toast({
variant: "default", variant: "default",
title: "Success" title: "Done",
description: res.data.data.message
}); });
// const id = res.data.data.resourceId; router.push(`/${orgId}/settings/blueprints`);
// const niceId = res.data.data.niceId;
// setNiceId(niceId);
// // Create targets if any exist
// if (targets.length > 0) {
// try {
// for (const target of targets) {
// const data: any = {
// ip: target.ip,
// port: target.port,
// method: target.method,
// enabled: target.enabled,
// siteId: target.siteId,
// hcEnabled: target.hcEnabled,
// hcPath: target.hcPath || null,
// hcMethod: target.hcMethod || null,
// hcInterval: target.hcInterval || null,
// hcTimeout: target.hcTimeout || null,
// hcHeaders: target.hcHeaders || null,
// hcScheme: target.hcScheme || null,
// hcHostname: target.hcHostname || null,
// hcPort: target.hcPort || null,
// hcFollowRedirects: target.hcFollowRedirects || null,
// hcStatus: target.hcStatus || null
// };
// // Only include path-related fields for HTTP resources
// if (isHttp) {
// data.path = target.path;
// data.pathMatchType = target.pathMatchType;
// data.rewritePath = target.rewritePath;
// data.rewritePathType = target.rewritePathType;
// data.priority = target.priority;
// }
// await api.put(`/resource/${id}/target`, data);
// }
// } catch (targetError) {
// console.error("Error creating targets:", targetError);
// toast({
// variant: "destructive",
// title: t("targetErrorCreate"),
// description: formatAxiosError(
// targetError,
// t("targetErrorCreateDescription")
// )
// });
// }
// }
// if (isHttp) {
// router.push(`/${orgId}/settings/resources/${niceId}`);
// } else {
// const tcpUdpData = tcpUdpForm.getValues();
// // Only show config snippets if enableProxy is explicitly true
// // if (tcpUdpData.enableProxy === true) {
// setShowSnippets(true);
// router.refresh();
// // } else {
// // // If enableProxy is false or undefined, go directly to resource page
// // router.push(`/${orgId}/settings/resources/${id}`);
// // }
// }
} }
} }
return ( return (