From 3633e02ff770ff7509a3d8c45b3829133e13e011 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 00:17:42 +0200 Subject: [PATCH 01/49] =?UTF-8?q?=F0=9F=94=A8=20run=20next=20server=20with?= =?UTF-8?q?=20turbopack=20(easy=20win)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/nextServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/nextServer.ts b/server/nextServer.ts index 78169f03..5302b9c8 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -9,7 +9,7 @@ const nextPort = config.getRawConfig().server.next_port; export async function createNextServer() { // const app = next({ dev }); - const app = next({ dev: process.env.ENVIRONMENT !== "prod" }); + const app = next({ dev: process.env.ENVIRONMENT !== "prod", turbopack: true }); const handle = app.getRequestHandler(); await app.prepare(); From 5fd104bb30c285091803828bea6fc45943769d2f Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 14:02:37 +0200 Subject: [PATCH 02/49] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20`bluePrintR?= =?UTF-8?q?uns`=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 3d6c6b0d..5ccc39c6 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -710,6 +710,16 @@ export const idpOrg = sqliteTable("idpOrg", { orgMapping: text("orgMapping") }); +// Blueprint runs +export const blueprintRuns = sqliteTable("blueprintRuns", { + blueprintRunId: text("blueprintRunId").primaryKey().notNull(), + createdAt: integer("createdAt", {mode: 'timestamp'}).notNull(), + succeeded: integer("succeeded", { mode: "boolean" }).notNull(), + contents: text("contents").notNull(), + message: text("message") +}); + + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; From ba745588e9457ea8dc2fd1d277f33c18958fbf10 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 21:55:09 +0200 Subject: [PATCH 03/49] =?UTF-8?q?=F0=9F=8E=A8=20format=20with=20prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 5ccc39c6..55ee35e8 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -142,11 +142,15 @@ export const targets = sqliteTable("targets", { }); export const targetHealthCheck = sqliteTable("targetHealthCheck", { - targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ autoIncrement: true }), + targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ + autoIncrement: true + }), targetId: integer("targetId") .notNull() .references(() => targets.targetId, { onDelete: "cascade" }), - hcEnabled: integer("hcEnabled", { mode: "boolean" }).notNull().default(false), + hcEnabled: integer("hcEnabled", { mode: "boolean" }) + .notNull() + .default(false), hcPath: text("hcPath"), hcScheme: text("hcScheme"), hcMode: text("hcMode").default("http"), @@ -156,7 +160,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds hcTimeout: integer("hcTimeout").default(5), // in seconds hcHeaders: text("hcHeaders"), - hcFollowRedirects: integer("hcFollowRedirects", { mode: "boolean" }).default(true), + hcFollowRedirects: integer("hcFollowRedirects", { + mode: "boolean" + }).default(true), hcMethod: text("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy" From d84ee3d03d45c8abfce341562e83317306035f8b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 21:55:41 +0200 Subject: [PATCH 04/49] =?UTF-8?q?=F0=9F=8C=90=20add=20blueprint=20section?= =?UTF-8?q?=20title=20in=20the=20sidebar=20in=20messages=20(`en-US`=20for?= =?UTF-8?q?=20now)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + 1 file changed, 1 insertion(+) diff --git a/messages/en-US.json b/messages/en-US.json index 4cffaf98..5c885bfc 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1151,6 +1151,7 @@ "sidebarLicense": "License", "sidebarClients": "Clients", "sidebarDomains": "Domains", + "sidebarBluePrints": "Blueprints", "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", From e575fae73b835a25a9a88f0fcf0dfc23ac04cdeb Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 21:56:10 +0200 Subject: [PATCH 05/49] =?UTF-8?q?=F0=9F=9A=A7=20SQLite=20database=20schema?= =?UTF-8?q?=20with=20modes=20(is=20it=20okay=20=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 55ee35e8..e15f8166 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -718,14 +718,17 @@ export const idpOrg = sqliteTable("idpOrg", { // Blueprint runs export const blueprintRuns = sqliteTable("blueprintRuns", { - blueprintRunId: text("blueprintRunId").primaryKey().notNull(), - createdAt: integer("createdAt", {mode: 'timestamp'}).notNull(), + blueprintRunId: integer("blueprintRunId").primaryKey({ + autoIncrement: true + }), + name: text("name").notNull(), + source: text("source", { enum: ["WEB", "CLI", "API"] }), + createdAt: integer("createdAt", { mode: "timestamp" }).notNull(), succeeded: integer("succeeded", { mode: "boolean" }).notNull(), contents: text("contents").notNull(), message: text("message") }); - export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; From 202d2075a692fdb9db6dc8884767f39404dd4ae7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 21:56:26 +0200 Subject: [PATCH 06/49] =?UTF-8?q?=F0=9F=9A=A7=20add=20blueprint=20to=20the?= =?UTF-8?q?=20sidebar=20and=20scaffold=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/blueprints/page.tsx | 14 ++++++++++++++ src/app/navigation.tsx | 13 +++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/app/[orgId]/settings/blueprints/page.tsx diff --git a/src/app/[orgId]/settings/blueprints/page.tsx b/src/app/[orgId]/settings/blueprints/page.tsx new file mode 100644 index 00000000..c37b88c1 --- /dev/null +++ b/src/app/[orgId]/settings/blueprints/page.tsx @@ -0,0 +1,14 @@ + + + + + +type BluePrintsPageProps = { + params: Promise<{ orgId: string }>; + searchParams: Promise<{ view?: string }>; +}; + +export default async function BluePrintsPage(props: BluePrintsPageProps) { + const params = await props.params; + return <> +} \ No newline at end of file diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 2d6aaec8..028a6f23 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -1,22 +1,22 @@ import { SidebarNavItem } from "@app/components/SidebarNav"; import { build } from "@server/build"; import { - Home, Settings, Users, Link as LinkIcon, Waypoints, Combine, Fingerprint, - Workflow, KeyRound, TicketCheck, User, Globe, // Added from 'dev' branch MonitorUp, // Added from 'dev' branch Server, - Zap, - CreditCard + CreditCard, + Bolt, + ScanText, + ReceiptText } from "lucide-react"; export type SidebarNavSection = { @@ -74,6 +74,11 @@ export const orgNavSections = ( title: "sidebarDomains", href: "/{orgId}/settings/domains", icon: + }, + { + title: "sidebarBluePrints", + href: "/{orgId}/settings/blueprints", + icon: } ] }, From 6521b66b7cbb141770384eb0a770c30a1872dec6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 21:58:19 +0200 Subject: [PATCH 07/49] =?UTF-8?q?=F0=9F=8D=B1=20add=20jsonschema=20for=20b?= =?UTF-8?q?lueprint=20yaml=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/schemas/blueprint.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/schemas/blueprint.json diff --git a/public/schemas/blueprint.json b/public/schemas/blueprint.json new file mode 100644 index 00000000..7c8effb4 --- /dev/null +++ b/public/schemas/blueprint.json @@ -0,0 +1 @@ +{"$ref":"#/definitions/BluePrintSchema","definitions":{"BluePrintSchema":{"type":"object","properties":{"proxy-resources":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"protocol":{"type":"string","enum":["http","tcp","udp"]},"ssl":{"type":"boolean"},"full-domain":{"type":"string"},"proxy-port":{"type":"integer","minimum":1,"maximum":65535},"enabled":{"type":"boolean"},"targets":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"site":{"type":"string"},"method":{"type":"string","enum":["http","https","h2c"]},"hostname":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"enabled":{"type":"boolean","default":true},"internal-port":{"type":"integer","minimum":1,"maximum":65535},"path":{"type":"string"},"path-match":{"anyOf":[{"anyOf":[{"not":{}},{"type":"string","enum":["exact","prefix","regex"]}]},{"type":"null"}]},"healthcheck":{"type":"object","properties":{"hostname":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"enabled":{"type":"boolean","default":true},"path":{"type":"string"},"scheme":{"type":"string"},"mode":{"type":"string","default":"http"},"interval":{"type":"integer","default":30},"unhealthyInterval":{"type":"integer","default":30},"timeout":{"type":"integer","default":5},"headers":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"],"additionalProperties":false}},{"type":"null"}],"default":null},"followRedirects":{"type":"boolean","default":true},"method":{"type":"string","default":"GET"},"status":{"type":"integer"}},"required":["hostname","port"],"additionalProperties":false},"rewritePath":{"type":"string"},"rewrite-match":{"anyOf":[{"anyOf":[{"not":{}},{"type":"string","enum":["exact","prefix","regex","stripPrefix"]}]},{"type":"null"}]},"priority":{"type":"integer","minimum":1,"maximum":1000,"default":100}},"required":["hostname","port"],"additionalProperties":false},{"type":"null"}]},"default":[]},"auth":{"type":"object","properties":{"pincode":{"type":"number","minimum":100000,"maximum":999999},"password":{"type":"string","minLength":1},"basic-auth":{"type":"object","properties":{"user":{"type":"string","minLength":1},"password":{"type":"string","minLength":1}},"required":["user","password"],"additionalProperties":false},"sso-enabled":{"type":"boolean","default":false},"sso-roles":{"type":"array","items":{"type":"string"},"default":[]},"sso-users":{"type":"array","items":{"type":"string","format":"email"},"default":[]},"whitelist-users":{"type":"array","items":{"type":"string","format":"email"},"default":[]}},"additionalProperties":false},"host-header":{"type":"string"},"tls-server-name":{"type":"string"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1},"value":{"type":"string","minLength":1}},"required":["name","value"],"additionalProperties":false}},"rules":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["allow","deny","pass"]},"match":{"type":"string","enum":["cidr","path","ip","country"]},"value":{"type":"string"}},"required":["action","match","value"],"additionalProperties":false}}},"additionalProperties":false},"default":{}},"client-resources":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":100},"site":{"type":"string","minLength":2,"maxLength":100},"protocol":{"type":"string","enum":["tcp","udp"]},"proxy-port":{"type":"number","minimum":1,"maximum":65535},"hostname":{"type":"string","minLength":1,"maxLength":255},"internal-port":{"type":"number","minimum":1,"maximum":65535},"enabled":{"type":"boolean","default":true}},"required":["name","protocol","proxy-port","hostname","internal-port"],"additionalProperties":false},"default":{}},"sites":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"docker-socket-enabled":{"type":"boolean","default":true}},"required":["name"],"additionalProperties":false},"default":{}}},"additionalProperties":false}},"$schema":"http://json-schema.org/draft-07/schema#"} \ No newline at end of file From 9024b2a974fdf2e032f44446c09446d565e5a3c2 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 23:49:13 +0200 Subject: [PATCH 08/49] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20finish=20db=20sch?= =?UTF-8?q?emas=20for=20blueprints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 12 ++++++++++++ server/db/sqlite/schema/schema.ts | 14 ++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 4bed23f8..2d05fcd1 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -671,6 +671,17 @@ export const setupTokens = pgTable("setupTokens", { dateUsed: varchar("dateUsed") }); +// Blueprint runs +export const blueprints = pgTable("blueprints", { + blueprintId: serial("blueprintId").primaryKey(), + name: varchar("name").notNull(), + source: varchar("source"), + createdAt: integer("createdAt").notNull(), + succeeded: boolean("succeeded").notNull(), + contents: text("contents").notNull(), + message: text("message") +}); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -722,3 +733,4 @@ export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; export type TargetHealthCheck = InferSelectModel; export type IdpOidcConfig = InferSelectModel; +export type Blueprint = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index e15f8166..ba6baebc 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -717,13 +717,18 @@ export const idpOrg = sqliteTable("idpOrg", { }); // Blueprint runs -export const blueprintRuns = sqliteTable("blueprintRuns", { - blueprintRunId: integer("blueprintRunId").primaryKey({ +export const blueprints = sqliteTable("blueprints", { + blueprintId: integer("blueprintId").primaryKey({ autoIncrement: true }), + orgId: text("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull(), name: text("name").notNull(), - source: text("source", { enum: ["WEB", "CLI", "API"] }), - createdAt: integer("createdAt", { mode: "timestamp" }).notNull(), + source: text("source"), + createdAt: integer("createdAt").notNull(), succeeded: integer("succeeded", { mode: "boolean" }).notNull(), contents: text("contents").notNull(), message: text("message") @@ -780,3 +785,4 @@ export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; export type TargetHealthCheck = InferSelectModel; export type IdpOidcConfig = InferSelectModel; +export type Blueprint = InferSelectModel; From 259cea1c42efe748bd3c0aa800598d7e35fb9b14 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 22 Oct 2025 23:49:43 +0200 Subject: [PATCH 09/49] =?UTF-8?q?=E2=9C=A8=20add=20API=20endpoint=20for=20?= =?UTF-8?q?listing=20blueprints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 4 +- server/routers/blueprints/index.ts | 1 + server/routers/blueprints/listBluePrints.ts | 129 ++++++++++++++++++++ server/routers/external.ts | 10 +- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 server/routers/blueprints/index.ts create mode 100644 server/routers/blueprints/listBluePrints.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index e48bc502..132eec7b 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -116,6 +116,9 @@ export enum ActionsEnum { updateLoginPage = "updateLoginPage", getLoginPage = "getLoginPage", deleteLoginPage = "deleteLoginPage", + + // blueprints + listBlueprints = "listBlueprints", applyBlueprint = "applyBlueprint" } @@ -193,7 +196,6 @@ export async function checkUserActionPermission( .limit(1); return roleActionPermission.length > 0; - } catch (error) { console.error("Error checking user action permission:", error); throw createHttpError( diff --git a/server/routers/blueprints/index.ts b/server/routers/blueprints/index.ts new file mode 100644 index 00000000..7182d5f9 --- /dev/null +++ b/server/routers/blueprints/index.ts @@ -0,0 +1 @@ +export * from "./listBluePrints"; diff --git a/server/routers/blueprints/listBluePrints.ts b/server/routers/blueprints/listBluePrints.ts new file mode 100644 index 00000000..531ced8a --- /dev/null +++ b/server/routers/blueprints/listBluePrints.ts @@ -0,0 +1,129 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, blueprints, orgs } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { sql, eq, or, inArray, and, count } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromZodError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { warn } from "console"; + +const listBluePrintsParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const listBluePrintsSchema = z + .object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) + }) + .strict(); + +async function queryBlueprints(orgId: string, limit: number, offset: number) { + const res = await db + .select({ + blueprintId: blueprints.blueprintId, + name: blueprints.name, + source: blueprints.source, + succeeded: blueprints.succeeded + }) + .from(blueprints) + .leftJoin(orgs, eq(blueprints.orgId, orgs.orgId)) + .limit(limit) + .offset(offset); + return res; +} + +export type ListBlueprintsResponse = { + domains: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/blueprints", + description: "List all blueprints for a organization.", + tags: [OpenAPITags.Org], + request: { + params: z.object({ + orgId: z.string() + }), + query: listBluePrintsSchema + }, + responses: {} +}); + +export async function listBlueprints( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listBluePrintsSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedQuery.error) + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const parsedParams = listBluePrintsParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedParams.error) + ) + ); + } + + const { orgId } = parsedParams.data; + + const blueprintsList = await queryBlueprints( + orgId.toString(), + limit, + offset + ); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(blueprints); + + return response(res, { + data: { + domains: blueprintsList, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Blueprints retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index 8bd72f62..a5ef3ba3 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -13,6 +13,7 @@ import * as siteResource from "./siteResource"; import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; +import * as blueprints from "./blueprints"; import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { @@ -675,8 +676,6 @@ authenticated.post( idp.updateOidcIdp ); - - authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -705,7 +704,6 @@ authenticated.get( idp.listIdpOrgPolicies ); - authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -814,6 +812,12 @@ authenticated.delete( domain.deleteAccountDomain ); +authenticated.get( + "/org/:orgId/blueprints", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listBlueprints), + blueprints.listBlueprints +); // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); From a5b48ab3924f48a914b12e6ed51056f0480ca693 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 00:13:31 +0200 Subject: [PATCH 10/49] =?UTF-8?q?=F0=9F=9A=A7=20blueprints=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 + src/app/[orgId]/settings/blueprints/page.tsx | 56 ++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 5c885bfc..0193cded 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1152,6 +1152,8 @@ "sidebarClients": "Clients", "sidebarDomains": "Domains", "sidebarBluePrints": "Blueprints", + "blueprints": "Blueprints", + "blueprintsDescription": "Blueprints are declarative YAML configurations that define your resources and their settings", "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", diff --git a/src/app/[orgId]/settings/blueprints/page.tsx b/src/app/[orgId]/settings/blueprints/page.tsx index c37b88c1..d71caf49 100644 --- a/src/app/[orgId]/settings/blueprints/page.tsx +++ b/src/app/[orgId]/settings/blueprints/page.tsx @@ -1,6 +1,13 @@ - - - +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import OrgProvider from "@app/providers/OrgProvider"; +import { ListBlueprintsResponse } from "@server/routers/blueprints"; +import { GetOrgResponse } from "@server/routers/org"; +import { AxiosResponse } from "axios"; +import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; +import { cache } from "react"; type BluePrintsPageProps = { @@ -8,7 +15,48 @@ type BluePrintsPageProps = { searchParams: Promise<{ view?: string }>; }; + export default async function BluePrintsPage(props: BluePrintsPageProps) { const params = await props.params; - return <> + + let blueprints: any[] = []; + try { + const res = await internal.get< + AxiosResponse + >(`/org/${params.orgId}/domains`, await authCookieHeader()); + + blueprints = res.data.data.domains as any[]; + } catch (e) { + console.error(e); + } + + let org = null; + try { + const getOrg = cache(async () => + internal.get>( + `/org/${params.orgId}`, + await authCookieHeader() + ) + ); + const res = await getOrg(); + org = res.data.data; + } catch { + redirect(`/${params.orgId}`); + } + + if (!org) { + } + + const t = await getTranslations(); + return ( + <> + + + {/* */} + + + ); } \ No newline at end of file From a534301eb71fd158936b7c35299436e8e0dee6c3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 00:26:41 +0200 Subject: [PATCH 11/49] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20make=20source=20not?= =?UTF-8?q?=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 2 +- server/db/sqlite/schema/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 2d05fcd1..622963a2 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -675,7 +675,7 @@ export const setupTokens = pgTable("setupTokens", { export const blueprints = pgTable("blueprints", { blueprintId: serial("blueprintId").primaryKey(), name: varchar("name").notNull(), - source: varchar("source"), + source: varchar("source").notNull(), createdAt: integer("createdAt").notNull(), succeeded: boolean("succeeded").notNull(), contents: text("contents").notNull(), diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index ba6baebc..cbce0048 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -727,7 +727,7 @@ export const blueprints = sqliteTable("blueprints", { }) .notNull(), name: text("name").notNull(), - source: text("source"), + source: text("source").notNull(), createdAt: integer("createdAt").notNull(), succeeded: integer("succeeded", { mode: "boolean" }).notNull(), contents: text("contents").notNull(), From 007d03e7f6dca3827b17e843d1999ddfc9cbc835 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 00:27:07 +0200 Subject: [PATCH 12/49] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/blueprints/listBluePrints.ts | 4 ++-- src/app/[orgId]/settings/blueprints/page.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/server/routers/blueprints/listBluePrints.ts b/server/routers/blueprints/listBluePrints.ts index 531ced8a..0aca58b0 100644 --- a/server/routers/blueprints/listBluePrints.ts +++ b/server/routers/blueprints/listBluePrints.ts @@ -49,7 +49,7 @@ async function queryBlueprints(orgId: string, limit: number, offset: number) { } export type ListBlueprintsResponse = { - domains: NonNullable>>; + blueprints: NonNullable>>; pagination: { total: number; limit: number; offset: number }; }; @@ -108,7 +108,7 @@ export async function listBlueprints( return response(res, { data: { - domains: blueprintsList, + blueprints: blueprintsList, pagination: { total: count, limit, diff --git a/src/app/[orgId]/settings/blueprints/page.tsx b/src/app/[orgId]/settings/blueprints/page.tsx index d71caf49..cd4c71c8 100644 --- a/src/app/[orgId]/settings/blueprints/page.tsx +++ b/src/app/[orgId]/settings/blueprints/page.tsx @@ -1,3 +1,4 @@ +import BlueprintsTable, { type BlueprintRow } from "@app/components/BlueprintsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; @@ -19,13 +20,13 @@ type BluePrintsPageProps = { export default async function BluePrintsPage(props: BluePrintsPageProps) { const params = await props.params; - let blueprints: any[] = []; + let blueprints: BlueprintRow[] = []; try { const res = await internal.get< AxiosResponse >(`/org/${params.orgId}/domains`, await authCookieHeader()); - blueprints = res.data.data.domains as any[]; + blueprints = res.data.data.blueprints } catch (e) { console.error(e); } @@ -55,7 +56,7 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) { title={t("blueprints")} description={t("blueprintsDescription")} /> - {/* */} + ); From fa6b7ca3ed388de1f9f5121387cb7917bb5a2ddd Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 00:33:49 +0200 Subject: [PATCH 13/49] =?UTF-8?q?=F0=9F=9A=A7=20(WIP)=20blueprints=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BlueprintsTable.tsx | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/components/BlueprintsTable.tsx diff --git a/src/components/BlueprintsTable.tsx b/src/components/BlueprintsTable.tsx new file mode 100644 index 00000000..10b9705b --- /dev/null +++ b/src/components/BlueprintsTable.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { DomainsDataTable } from "@app/components/DomainsDataTable"; +import { Button } from "@app/components/ui/button"; +import { ArrowUpDown } from "lucide-react"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Badge } from "@app/components/ui/badge"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import CreateDomainForm from "@app/components/CreateDomainForm"; +import { useToast } from "@app/hooks/useToast"; +import { useOrgContext } from "@app/hooks/useOrgContext"; + +export type BlueprintRow = { + blueprintId: number; + source: string; + succeeded: boolean; + name: string; +}; + +type Props = { + blueprints: BlueprintRow[]; +}; + +export default function BlueprintsTable({ blueprints }: Props) { + return <> +} \ No newline at end of file From e30fde523713d87039054b0b2fd7979ced96627f Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 22:14:09 +0200 Subject: [PATCH 14/49] =?UTF-8?q?=F0=9F=92=84=20blueprint=20data=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 3 + server/routers/blueprints/listBluePrints.ts | 14 +- src/app/[orgId]/settings/blueprints/page.tsx | 10 +- src/components/BlueprintsTable.tsx | 179 +++++++++++++++++-- 4 files changed, 191 insertions(+), 15 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 0193cded..064aee7a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1154,6 +1154,9 @@ "sidebarBluePrints": "Blueprints", "blueprints": "Blueprints", "blueprintsDescription": "Blueprints are declarative YAML configurations that define your resources and their settings", + "blueprintAdd": "Add Blueprint", + "searchBlueprintProgress": "Search blueprints...", + "source": "Source", "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", diff --git a/server/routers/blueprints/listBluePrints.ts b/server/routers/blueprints/listBluePrints.ts index 0aca58b0..e25f0563 100644 --- a/server/routers/blueprints/listBluePrints.ts +++ b/server/routers/blueprints/listBluePrints.ts @@ -39,7 +39,8 @@ async function queryBlueprints(orgId: string, limit: number, offset: number) { blueprintId: blueprints.blueprintId, name: blueprints.name, source: blueprints.source, - succeeded: blueprints.succeeded + succeeded: blueprints.succeeded, + orgId: blueprints.orgId }) .from(blueprints) .leftJoin(orgs, eq(blueprints.orgId, orgs.orgId)) @@ -48,8 +49,15 @@ async function queryBlueprints(orgId: string, limit: number, offset: number) { return res; } +type BlueprintData = Omit< + Awaited>[number], + "source" +> & { + source: "API" | "WEB" | "CLI"; +}; + export type ListBlueprintsResponse = { - blueprints: NonNullable>>; + blueprints: NonNullable; pagination: { total: number; limit: number; offset: number }; }; @@ -108,7 +116,7 @@ export async function listBlueprints( return response(res, { data: { - blueprints: blueprintsList, + blueprints: blueprintsList as BlueprintData[], pagination: { total: count, limit, diff --git a/src/app/[orgId]/settings/blueprints/page.tsx b/src/app/[orgId]/settings/blueprints/page.tsx index cd4c71c8..e53aae9e 100644 --- a/src/app/[orgId]/settings/blueprints/page.tsx +++ b/src/app/[orgId]/settings/blueprints/page.tsx @@ -24,13 +24,17 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) { try { const res = await internal.get< AxiosResponse - >(`/org/${params.orgId}/domains`, await authCookieHeader()); + >(`/org/${params.orgId}/blueprints`, await authCookieHeader()); blueprints = res.data.data.blueprints + console.log({ + ...res.data.data + }) } catch (e) { console.error(e); } + let org = null; try { const getOrg = cache(async () => @@ -49,6 +53,8 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) { } const t = await getTranslations(); + + return ( <> @@ -56,7 +62,7 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) { title={t("blueprints")} description={t("blueprintsDescription")} /> - + ); diff --git a/src/components/BlueprintsTable.tsx b/src/components/BlueprintsTable.tsx index 10b9705b..c5509bbd 100644 --- a/src/components/BlueprintsTable.tsx +++ b/src/components/BlueprintsTable.tsx @@ -3,8 +3,8 @@ import { ColumnDef } from "@tanstack/react-table"; import { DomainsDataTable } from "@app/components/DomainsDataTable"; import { Button } from "@app/components/ui/button"; -import { ArrowUpDown } from "lucide-react"; -import { useState } from "react"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { useState, useTransition } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; @@ -15,18 +15,177 @@ import { useTranslations } from "next-intl"; import CreateDomainForm from "@app/components/CreateDomainForm"; import { useToast } from "@app/hooks/useToast"; import { useOrgContext } from "@app/hooks/useOrgContext"; +import { DataTable } from "./ui/data-table"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; +import Link from "next/link"; +import { ListBlueprintsResponse } from "@server/routers/blueprints"; -export type BlueprintRow = { - blueprintId: number; - source: string; - succeeded: boolean; - name: string; -}; +export type BlueprintRow = ListBlueprintsResponse['blueprints'][number] type Props = { blueprints: BlueprintRow[]; + orgId: string; }; -export default function BlueprintsTable({ blueprints }: Props) { - return <> +export default function BlueprintsTable({ blueprints, orgId }: Props) { + + const t = useTranslations(); + + const [isRefreshing, startTransition] = useTransition() + const router = useRouter() + + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "source", + header: ({ column }) => { + return ( + + ); + }, + // cell: ({ row }) => { + // const originalRow = row.original; + // if ( + // originalRow.type == "newt" || + // originalRow.type == "wireguard" + // ) { + // if (originalRow.online) { + // return ( + // + //
+ // {t("online")} + //
+ // ); + // } else { + // return ( + // + //
+ // {t("offline")} + //
+ // ); + // } + // } else { + // return -; + // } + // } + }, + // { + // accessorKey: "nice", + // header: ({ column }) => { + // return ( + // + // ); + // }, + // // cell: ({ row }) => { + // // return ( + // //
+ // // {row.original.nice} + // //
+ // // ); + // // } + // }, + // { + // id: "actions", + // cell: ({ row }) => { + // const siteRow = row.original; + // return ( + //
+ // + // + // + // + // + // + // + // {t("viewSettings")} + // + // + // { + // // setSelectedSite(siteRow); + // // setIsDeleteModalOpen(true); + // }} + // > + // + // {t("delete")} + // + // + // + // + + // + // + // + //
+ // ); + // } + // } + ]; + + return { + router.push(`/${orgId}/settings/blueprints/create`); + }} + addButtonText={t('blueprintAdd')} + onRefresh={() => { + startTransition(() => router.refresh()) + }} + isRefreshing={isRefreshing} + defaultSort={{ + id: "name", + desc: false + }} + /> } \ No newline at end of file From 90ddffce0eaf25427b52468febb40bccfc732aa5 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 22:27:14 +0200 Subject: [PATCH 15/49] =?UTF-8?q?=F0=9F=9A=A7=20create=20blueprint=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/blueprints/create/page.tsx | 9 ++++ src/app/[orgId]/settings/blueprints/page.tsx | 46 +++++++++---------- src/app/[orgId]/settings/layout.tsx | 25 +++++----- 3 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 src/app/[orgId]/settings/blueprints/create/page.tsx diff --git a/src/app/[orgId]/settings/blueprints/create/page.tsx b/src/app/[orgId]/settings/blueprints/create/page.tsx new file mode 100644 index 00000000..6ce6bc9a --- /dev/null +++ b/src/app/[orgId]/settings/blueprints/create/page.tsx @@ -0,0 +1,9 @@ +import type { Metadata } from "next"; + +export interface CreateBlueprintPageProps { + params: Promise<{ orgId: string }>; +} + +export default function CreateBlueprintPage(props: CreateBlueprintPageProps) { + return <>; +} diff --git a/src/app/[orgId]/settings/blueprints/page.tsx b/src/app/[orgId]/settings/blueprints/page.tsx index e53aae9e..21c13c35 100644 --- a/src/app/[orgId]/settings/blueprints/page.tsx +++ b/src/app/[orgId]/settings/blueprints/page.tsx @@ -1,4 +1,6 @@ -import BlueprintsTable, { type BlueprintRow } from "@app/components/BlueprintsTable"; +import BlueprintsTable, { + type BlueprintRow +} from "@app/components/BlueprintsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; @@ -6,35 +8,35 @@ import OrgProvider from "@app/providers/OrgProvider"; import { ListBlueprintsResponse } from "@server/routers/blueprints"; import { GetOrgResponse } from "@server/routers/org"; import { AxiosResponse } from "axios"; +import { Metadata } from "next"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { cache } from "react"; - type BluePrintsPageProps = { params: Promise<{ orgId: string }>; searchParams: Promise<{ view?: string }>; }; +export const metadata: Metadata = { + title: "Blueprint" +}; export default async function BluePrintsPage(props: BluePrintsPageProps) { const params = await props.params; let blueprints: BlueprintRow[] = []; try { - const res = await internal.get< - AxiosResponse - >(`/org/${params.orgId}/blueprints`, await authCookieHeader()); + const res = await internal.get>( + `/org/${params.orgId}/blueprints`, + await authCookieHeader() + ); - blueprints = res.data.data.blueprints - console.log({ - ...res.data.data - }) + blueprints = res.data.data.blueprints; } catch (e) { console.error(e); } - let org = null; try { const getOrg = cache(async () => @@ -49,21 +51,15 @@ export default async function BluePrintsPage(props: BluePrintsPageProps) { redirect(`/${params.orgId}`); } - if (!org) { - } - const t = await getTranslations(); - return ( - <> - - - - - - ); -} \ No newline at end of file + + + + + ); +} diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index d35af6e6..5b12ea3a 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -1,25 +1,15 @@ import { Metadata } from "next"; -import { - Combine, - KeyRound, - LinkIcon, - Settings, - Users, - Waypoints, - Workflow -} from "lucide-react"; + import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; -import { ListOrgsResponse } from "@server/routers/org"; -import { GetOrgResponse, ListUserOrgsResponse } from "@server/routers/org"; +import { ListUserOrgsResponse } from "@server/routers/org"; import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; import { GetOrgUserResponse } from "@server/routers/user"; import UserProvider from "@app/providers/UserProvider"; import { Layout } from "@app/components/Layout"; -import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav"; import { getTranslations } from "next-intl/server"; import { pullEnv } from "@app/lib/pullEnv"; import { orgNavSections } from "@app/app/navigation"; @@ -27,7 +17,10 @@ import { orgNavSections } from "@app/app/navigation"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { - title: `Settings - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, + title: { + template: `%s - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, + default: `Settings - ${process.env.BRANDING_APP_NAME || "Pangolin"}` + }, description: "" }; @@ -86,7 +79,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( - + {children} From 23b13f0a0eae78900297391a28c92df037ccdc2a Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 23 Oct 2025 23:10:21 +0200 Subject: [PATCH 16/49] =?UTF-8?q?=F0=9F=92=84=20add=20toploader=20navigati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 34 ++++++++++++++++++++++++++++++ package.json | 3 +++ src/app/globals.css | 6 +++++- src/app/layout.tsx | 8 ++++--- src/components/Toploader.tsx | 41 ++++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/components/Toploader.tsx diff --git a/package-lock.json b/package-lock.json index f03f5a87..d098fa01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,10 +72,12 @@ "next": "15.5.6", "next-intl": "^4.3.12", "next-themes": "0.4.6", + "nextjs-toploader": "^3.9.17", "node-cache": "5.1.2", "node-fetch": "3.3.2", "nodemailer": "7.0.9", "npm": "^11.6.2", + "nprogress": "^0.2.0", "oslo": "1.2.1", "pg": "^8.16.2", "posthog-node": "^5.9.5", @@ -118,6 +120,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "24.8.1", "@types/nodemailer": "7.0.2", + "@types/nprogress": "^0.2.3", "@types/pg": "8.15.5", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", @@ -8691,6 +8694,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/nprogress": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz", + "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/pg": { "version": "8.15.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", @@ -15197,6 +15207,24 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nextjs-toploader": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz", + "integrity": "sha512-9OF0KSSLtoSAuNg2LZ3aTl4hR9mBDj5L9s9DZiFCbMlXehyICGjkIz5dVGzuATU2bheJZoBdFgq9w07AKSuQQw==", + "license": "MIT", + "dependencies": { + "nprogress": "^0.2.0", + "prop-types": "^15.8.1" + }, + "funding": { + "url": "https://buymeacoffee.com/thesgj" + }, + "peerDependencies": { + "next": ">= 6.0.0", + "react": ">= 16.0.0", + "react-dom": ">= 16.0.0" + } + }, "node_modules/node-abi": { "version": "3.78.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", @@ -17727,6 +17755,12 @@ "inBundle": true, "license": "ISC" }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", diff --git a/package.json b/package.json index 44985a2f..029f5840 100644 --- a/package.json +++ b/package.json @@ -95,10 +95,12 @@ "next": "15.5.6", "next-intl": "^4.3.12", "next-themes": "0.4.6", + "nextjs-toploader": "^3.9.17", "node-cache": "5.1.2", "node-fetch": "3.3.2", "nodemailer": "7.0.9", "npm": "^11.6.2", + "nprogress": "^0.2.0", "oslo": "1.2.1", "pg": "^8.16.2", "posthog-node": "^5.9.5", @@ -141,6 +143,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "24.8.1", "@types/nodemailer": "7.0.2", + "@types/nprogress": "^0.2.3", "@types/pg": "8.15.5", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", diff --git a/src/app/globals.css b/src/app/globals.css index e643cfb6..1147e37e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -40,7 +40,7 @@ } .dark { - --background: oklch(0.20 0.006 285.885); + --background: oklch(0.2 0.006 285.885); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); --card-foreground: oklch(0.985 0 0); @@ -140,3 +140,7 @@ p { word-break: keep-all; white-space: normal; } + +#nprogress .bar { + background: var(--color-primary) !important; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5cd083b8..e498d7da 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,6 +18,7 @@ import { NextIntlClientProvider } from "next-intl"; import { getLocale } from "next-intl/server"; import { Toaster } from "@app/components/ui/toaster"; import { build } from "@server/build"; +import { TopLoader } from "@app/components/Toploader"; export const metadata: Metadata = { title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, @@ -62,9 +63,9 @@ export default async function RootLayout({ if (build === "enterprise") { const licenseStatusRes = await cache( async () => - await priv.get>( - "/license/status" - ) + await priv.get>( + "/license/status" + ) )(); licenseStatus = licenseStatusRes.data.data; } else if (build === "saas") { @@ -84,6 +85,7 @@ export default async function RootLayout({ return ( + + + + + ); +} + +function FinishingLoader() { + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); + React.useEffect(() => { + NProgress.done(); + }, [pathname, router, searchParams]); + React.useEffect(() => { + const linkClickListener = (ev: MouseEvent) => { + const element = ev.target as HTMLElement; + const closestlink = element.closest("a"); + const isOpenToNewTabClick = + ev.ctrlKey || + ev.shiftKey || + ev.metaKey || // apple + (ev.button && ev.button == 1); // middle click, >IE9 + everyone else + + if (closestlink && isOpenToNewTabClick) { + NProgress.done(); + } + }; + window.addEventListener("click", linkClickListener); + return () => window.removeEventListener("click", linkClickListener); + }, []); + return null; +} From 038f8829c2c14a6e18157b910f106102de8632ff Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 24 Oct 2025 04:17:13 +0200 Subject: [PATCH 17/49] =?UTF-8?q?=F0=9F=9A=A7=20create=20blueprint=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 7 + next.config.mjs | 3 +- package-lock.json | 61 ++++ package.json | 1 + public/schemas/blueprint.json | 1 - .../settings/blueprints/create/page.tsx | 38 ++- src/components/CreateBlueprintForm.tsx | 264 ++++++++++++++++++ src/components/ui/button.tsx | 12 +- 8 files changed, 381 insertions(+), 6 deletions(-) delete mode 100644 public/schemas/blueprint.json create mode 100644 src/components/CreateBlueprintForm.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 064aee7a..ba5e6f7b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1155,8 +1155,15 @@ "blueprints": "Blueprints", "blueprintsDescription": "Blueprints are declarative YAML configurations that define your resources and their settings", "blueprintAdd": "Add Blueprint", + "blueprintGoBack": "Back to blueprints", + "blueprintCreate": "Create blueprint", + "blueprintCreateDescription2": "Follow the steps below to create and apply a new blueprint", + "blueprintInfo": "Blueprint Information", + "blueprintNameDescription": "This is the display name for the blueprint.", + "blueprintContentsDescription": "Define the YAML content describing your infrastructure", "searchBlueprintProgress": "Search blueprints...", "source": "Source", + "contents": "Contents", "enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.", "enableDockerSocketLink": "Learn More", diff --git a/next.config.mjs b/next.config.mjs index c870f1c1..d771dbca 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,7 +7,8 @@ const nextConfig = { eslint: { ignoreDuringBuilds: true }, - output: "standalone" + output: "standalone", + }; export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index d098fa01..f4a112c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@asteasolutions/zod-to-openapi": "^7.3.4", "@aws-sdk/client-s3": "3.908.0", "@hookform/resolvers": "5.2.2", + "@monaco-editor/react": "^4.7.0", "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", @@ -3857,6 +3858,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@monaco-editor/loader": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz", + "integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -11200,6 +11224,13 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -14980,6 +15011,30 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", + "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.1.7", + "marked": "14.0.0" + } + }, + "node_modules/monaco-editor/node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/motion-dom": { "version": "12.23.23", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", @@ -20587,6 +20642,12 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index 029f5840..88a7bb67 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@asteasolutions/zod-to-openapi": "^7.3.4", "@aws-sdk/client-s3": "3.908.0", "@hookform/resolvers": "5.2.2", + "@monaco-editor/react": "^4.7.0", "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", diff --git a/public/schemas/blueprint.json b/public/schemas/blueprint.json deleted file mode 100644 index 7c8effb4..00000000 --- a/public/schemas/blueprint.json +++ /dev/null @@ -1 +0,0 @@ -{"$ref":"#/definitions/BluePrintSchema","definitions":{"BluePrintSchema":{"type":"object","properties":{"proxy-resources":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"protocol":{"type":"string","enum":["http","tcp","udp"]},"ssl":{"type":"boolean"},"full-domain":{"type":"string"},"proxy-port":{"type":"integer","minimum":1,"maximum":65535},"enabled":{"type":"boolean"},"targets":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"site":{"type":"string"},"method":{"type":"string","enum":["http","https","h2c"]},"hostname":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"enabled":{"type":"boolean","default":true},"internal-port":{"type":"integer","minimum":1,"maximum":65535},"path":{"type":"string"},"path-match":{"anyOf":[{"anyOf":[{"not":{}},{"type":"string","enum":["exact","prefix","regex"]}]},{"type":"null"}]},"healthcheck":{"type":"object","properties":{"hostname":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"enabled":{"type":"boolean","default":true},"path":{"type":"string"},"scheme":{"type":"string"},"mode":{"type":"string","default":"http"},"interval":{"type":"integer","default":30},"unhealthyInterval":{"type":"integer","default":30},"timeout":{"type":"integer","default":5},"headers":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"],"additionalProperties":false}},{"type":"null"}],"default":null},"followRedirects":{"type":"boolean","default":true},"method":{"type":"string","default":"GET"},"status":{"type":"integer"}},"required":["hostname","port"],"additionalProperties":false},"rewritePath":{"type":"string"},"rewrite-match":{"anyOf":[{"anyOf":[{"not":{}},{"type":"string","enum":["exact","prefix","regex","stripPrefix"]}]},{"type":"null"}]},"priority":{"type":"integer","minimum":1,"maximum":1000,"default":100}},"required":["hostname","port"],"additionalProperties":false},{"type":"null"}]},"default":[]},"auth":{"type":"object","properties":{"pincode":{"type":"number","minimum":100000,"maximum":999999},"password":{"type":"string","minLength":1},"basic-auth":{"type":"object","properties":{"user":{"type":"string","minLength":1},"password":{"type":"string","minLength":1}},"required":["user","password"],"additionalProperties":false},"sso-enabled":{"type":"boolean","default":false},"sso-roles":{"type":"array","items":{"type":"string"},"default":[]},"sso-users":{"type":"array","items":{"type":"string","format":"email"},"default":[]},"whitelist-users":{"type":"array","items":{"type":"string","format":"email"},"default":[]}},"additionalProperties":false},"host-header":{"type":"string"},"tls-server-name":{"type":"string"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1},"value":{"type":"string","minLength":1}},"required":["name","value"],"additionalProperties":false}},"rules":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["allow","deny","pass"]},"match":{"type":"string","enum":["cidr","path","ip","country"]},"value":{"type":"string"}},"required":["action","match","value"],"additionalProperties":false}}},"additionalProperties":false},"default":{}},"client-resources":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":100},"site":{"type":"string","minLength":2,"maxLength":100},"protocol":{"type":"string","enum":["tcp","udp"]},"proxy-port":{"type":"number","minimum":1,"maximum":65535},"hostname":{"type":"string","minLength":1,"maxLength":255},"internal-port":{"type":"number","minimum":1,"maximum":65535},"enabled":{"type":"boolean","default":true}},"required":["name","protocol","proxy-port","hostname","internal-port"],"additionalProperties":false},"default":{}},"sites":{"type":"object","additionalProperties":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"docker-socket-enabled":{"type":"boolean","default":true}},"required":["name"],"additionalProperties":false},"default":{}}},"additionalProperties":false}},"$schema":"http://json-schema.org/draft-07/schema#"} \ No newline at end of file diff --git a/src/app/[orgId]/settings/blueprints/create/page.tsx b/src/app/[orgId]/settings/blueprints/create/page.tsx index 6ce6bc9a..387e470a 100644 --- a/src/app/[orgId]/settings/blueprints/create/page.tsx +++ b/src/app/[orgId]/settings/blueprints/create/page.tsx @@ -1,9 +1,43 @@ +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Button } from "@app/components/ui/button"; +import { getTranslations } from "next-intl/server"; +import Link from "next/link"; + import type { Metadata } from "next"; +import { ArrowLeft } from "lucide-react"; +import CreateBlueprintForm from "@app/components/CreateBlueprintForm"; export interface CreateBlueprintPageProps { params: Promise<{ orgId: string }>; } -export default function CreateBlueprintPage(props: CreateBlueprintPageProps) { - return <>; +export const metadata: Metadata = { + title: "Create blueprint" +}; + +export default async function CreateBlueprintPage( + props: CreateBlueprintPageProps +) { + const t = await getTranslations(); + + const orgId = (await props.params).orgId; + + return ( + <> +
+ + +
+ + + + ); } diff --git a/src/components/CreateBlueprintForm.tsx b/src/components/CreateBlueprintForm.tsx new file mode 100644 index 00000000..de8516c2 --- /dev/null +++ b/src/components/CreateBlueprintForm.tsx @@ -0,0 +1,264 @@ +"use client"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { useTranslations } from "next-intl"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import z from "zod"; +import { useForm } from "react-hook-form"; +import { Input } from "./ui/input"; +import { useActionState, useTransition } from "react"; +import Editor, { useMonaco } from "@monaco-editor/react"; +import { cn } from "@app/lib/cn"; +import { Button } from "./ui/button"; + +export type CreateBlueprintFormProps = {}; + +export default function CreateBlueprintForm({}: CreateBlueprintFormProps) { + const t = useTranslations(); + + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + const baseForm = useForm({ + resolver: zodResolver( + z.object({ + name: z.string().min(1).max(255), + contents: z.string() + }) + ), + defaultValues: { + name: "", + contents: `proxy-resources: + resource-nice-id-uno: + name: this is my resource + protocol: http + full-domain: duce.test.example.com + host-header: example.com + tls-server-name: example.com +` + } + }); + + async function onSubmit() { + // setCreateLoading(true); + // const baseData = baseForm.getValues(); + // const isHttp = baseData.http; + // try { + // const payload = { + // name: baseData.name, + // http: baseData.http, + // }; + // let sanitizedSubdomain: string | undefined; + // if (isHttp) { + // const httpData = httpForm.getValues(); + // sanitizedSubdomain = httpData.subdomain + // ? finalizeSubdomainSanitize(httpData.subdomain) + // : undefined; + // Object.assign(payload, { + // subdomain: sanitizedSubdomain + // ? toASCII(sanitizedSubdomain) + // : undefined, + // domainId: httpData.domainId, + // protocol: "tcp" + // }); + // } else { + // const tcpUdpData = tcpUdpForm.getValues(); + // Object.assign(payload, { + // protocol: tcpUdpData.protocol, + // proxyPort: tcpUdpData.proxyPort + // // enableProxy: tcpUdpData.enableProxy + // }); + // } + // const res = await api + // .put< + // AxiosResponse + // >(`/org/${orgId}/resource/`, payload) + // .catch((e) => { + // toast({ + // variant: "destructive", + // title: t("resourceErrorCreate"), + // description: formatAxiosError( + // e, + // t("resourceErrorCreateDescription") + // ) + // }); + // }); + // if (res && res.status === 201) { + // const id = res.data.data.resourceId; + // 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}`); + // // } + // } + // } + // } catch (e) { + // console.error(t("resourceErrorCreateMessage"), e); + // toast({ + // variant: "destructive", + // title: t("resourceErrorCreate"), + // description: t("resourceErrorCreateMessageDescription") + // }); + // } + // setCreateLoading(false); + } + return ( +
+ + + + + + {t("blueprintInfo")} + + + + + ( + + {t("name")} + + + + + + {t("blueprintNameDescription")} + + + )} + /> + + + + + + + + {t("contents")} + + + {t("blueprintContentsDescription")} + + + +
+ setChangedContents(value ?? "")} + /> + +