+ );
+}
diff --git a/src/app/(dashboard)/_projects/CreateProject/3a_FromTemplate.tsx b/src/app/(dashboard)/_projects/CreateProject/3a_FromTemplate.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/(dashboard)/_projects/CreateProject/3b_FromCompose.tsx b/src/app/(dashboard)/_projects/CreateProject/3b_FromCompose.tsx
new file mode 100644
index 0000000..427053c
--- /dev/null
+++ b/src/app/(dashboard)/_projects/CreateProject/3b_FromCompose.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import React, { Suspense, useEffect } from "react";
+import { useFormContext } from "react-hook-form";
+import { toast } from "sonner";
+import { type z } from "zod";
+import { Button } from "~/components/ui/button";
+import { Input } from "~/components/ui/input";
+import { Label } from "~/components/ui/label";
+import { type ProjectCreationValidator } from "./CreateProject";
+
+// kinda large dependency, but syntax highlighting is nice
+const Editor = React.lazy(() => import("../YAMLEditor"));
+
+export default function CreateProjectFromCompose() {
+ const form = useFormContext>();
+
+ const fetchComposeFile = useQuery({
+ queryKey: ["fetchComposeFile", form.getValues().composeURL],
+ queryFn: async () => {
+ const fileURL = form.getValues().composeFile;
+
+ if (!fileURL) {
+ throw new Error("Invalid URL specified");
+ }
+
+ return await fetch(fileURL).then((res) => res.text());
+ },
+ enabled: false,
+ // cacheTime: 0,
+ retry: false,
+ });
+
+ useEffect(() => {
+ if (fetchComposeFile.isError) {
+ toast.error("Failed to fetch Docker Compose file");
+ } else if (fetchComposeFile.isSuccess) {
+ form.setValue("composeFile", fetchComposeFile.data);
+ toast.success("Fetched Docker Compose file");
+ }
+ }, [fetchComposeFile, form]);
+
+ return (
+
+
+
+ {/* Maybe we should add the ability to fetch URLs protected by CORS by routing through the server
+ Currently sites like pastebin.com will not work due to strict CORS policies
+
+ If we do this, make sure the user cannot access private URLs/IPs and proper input validation is done */}
+
+
+
+
+
+ Loading the editor...
}>
+ form.setValue("composeFile", data)}
+ />
+
+
+ );
+}
diff --git a/src/app/(dashboard)/_projects/CreateProject/CreateProject.tsx b/src/app/(dashboard)/_projects/CreateProject/CreateProject.tsx
new file mode 100644
index 0000000..2450982
--- /dev/null
+++ b/src/app/(dashboard)/_projects/CreateProject/CreateProject.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+import { stripIndents } from "common-tags";
+import { AnimatePresence, motion } from "framer-motion";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { z } from "zod";
+import { Button } from "~/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "~/components/ui/dialog";
+import { Form } from "~/components/ui/form";
+import { useForm } from "~/hooks/forms";
+import { zDockerName } from "~/server/utils/zod";
+import { api } from "~/trpc/react";
+import CreateProjectBasicDetails from "./1_BasicDetails";
+import CreateProjectSelectType from "./2_SelectType";
+import CreateProjectFromCompose from "./3b_FromCompose";
+
+export enum CreateProjectType {
+ Template = "template",
+ Compose = "compose",
+ Blank = "blank",
+}
+
+export enum CreateProjectStep {
+ BasicDetails,
+ ChooseType,
+
+ FromTemplate,
+ FromCompose,
+ FromBlank,
+}
+
+export const ProjectCreationValidator = z.object({
+ step: z.nativeEnum(CreateProjectStep), // internally used
+
+ type: z.nativeEnum(CreateProjectType),
+ internalName: zDockerName,
+ name: z.string(),
+
+ // compose
+ composeURL: z.string().url().optional(),
+ composeFile: z.string(),
+
+ // template
+ template: z.string().optional(),
+});
+
+/**
+ * Flow
+ * 1. Choose Type
+ * |-> From Template -> Choose Template Page -> Basic Details
+ * |-> From Compose -> Enter URL -> Fetch -> Basic Details
+ * \-> From Blank -> Basic Details
+ *
+ * 2. Basic Details -> Submit
+ *
+ * @returns
+ */
+export function CreateProjectButton() {
+ const create = api.projects.create.useMutation({
+ onError(error) {
+ toast.error(error.message);
+ },
+ });
+
+ const form = useForm(ProjectCreationValidator, {
+ defaultValues: {
+ step: CreateProjectStep.ChooseType,
+
+ composeFile: stripIndents`
+ version: '3.8'
+
+ services:
+ # Everything written here will be transformed into a HostForge service.
+ # Please note that some options are not supported by Docker Swarm and not all options are supported by HostForge.`,
+ },
+ });
+
+ const router = useRouter();
+ const [step, type] = form.watch(["step", "type"]);
+
+ return (
+
+ );
+}
diff --git a/src/app/(dashboard)/_projects/ProjectList.tsx b/src/app/(dashboard)/_projects/ProjectList.tsx
index 780de29..827b89f 100644
--- a/src/app/(dashboard)/_projects/ProjectList.tsx
+++ b/src/app/(dashboard)/_projects/ProjectList.tsx
@@ -14,7 +14,7 @@ import {
} from "~/components/ui/dropdown-menu";
import { api } from "~/trpc/react";
import { type RouterOutputs } from "~/trpc/shared";
-import { CreateProjectButton } from "./CreateProject";
+import { CreateProjectButton } from "./CreateProject/CreateProject";
import { Project } from "./Project";
type Projects = RouterOutputs["projects"]["list"];
diff --git a/src/app/(dashboard)/project/[projectId]/ProjectLayout.tsx b/src/app/(dashboard)/project/[projectId]/ProjectLayout.tsx
index 5ef25e1..9377f12 100644
--- a/src/app/(dashboard)/project/[projectId]/ProjectLayout.tsx
+++ b/src/app/(dashboard)/project/[projectId]/ProjectLayout.tsx
@@ -103,7 +103,7 @@ export function ProjectLayout(props: {
))}
{project.data.services.length == 0 && (
- No services found.
+ No services.
)}
diff --git a/src/app/(dashboard)/project/[projectId]/service/[serviceId]/advanced/DeploymentSettings.tsx b/src/app/(dashboard)/project/[projectId]/service/[serviceId]/advanced/DeploymentSettings.tsx
index 608a776..9eb3645 100644
--- a/src/app/(dashboard)/project/[projectId]/service/[serviceId]/advanced/DeploymentSettings.tsx
+++ b/src/app/(dashboard)/project/[projectId]/service/[serviceId]/advanced/DeploymentSettings.tsx
@@ -61,7 +61,7 @@ export default function DeploymentSettings({
})}
className="grid grid-cols-2 gap-4"
>
-
Deployment
+
Deployment
}
/>
+ {/*
Resource Limits
+
+ */}
+
diff --git a/src/app/(dashboard)/project/[projectId]/service/[serviceId]/containers/ContainersList.tsx b/src/app/(dashboard)/project/[projectId]/service/[serviceId]/containers/ContainersList.tsx
index 15b4ff9..1748c08 100644
--- a/src/app/(dashboard)/project/[projectId]/service/[serviceId]/containers/ContainersList.tsx
+++ b/src/app/(dashboard)/project/[projectId]/service/[serviceId]/containers/ContainersList.tsx
@@ -52,9 +52,9 @@ export default function ContainersPage({
{containers.data && containers.data.latest.length === 0 && (
-
- No containers running. Try hitting the Deploy Changes button to
- deploy the service.
+
+ No containers running. Try hitting the{" "}
+ Deploy Changes button to deploy the service.
)}
diff --git a/src/server/docker/stack.ts b/src/server/docker/stack.ts
index 7b13851..c79330b 100644
--- a/src/server/docker/stack.ts
+++ b/src/server/docker/stack.ts
@@ -62,7 +62,7 @@ export async function buildDockerStackFile(
limits: {
cpus: service.max_cpu?.toString() ?? undefined,
memory: service.max_memory ?? undefined,
- pids: service.max_pids ?? undefined,
+ // pids: service.max_pids ?? undefined,
},
},