From 038f8829c2c14a6e18157b910f106102de8632ff Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 24 Oct 2025 04:17:13 +0200 Subject: [PATCH] =?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 ?? "")} + /> + +