diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ba57ec7b --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy" +PORT=3000 +NODE_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..71a3816e --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +/redis-data +traefik.yml + +# testing +/coverage +/dist +/production-server +# database +/prisma/db.sqlite +/prisma/db.sqlite-journal +/logs + +# next.js +/.next/ +/out/ +next-env.d.ts +/dokploy +/config + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# otros +/.data +/.main + +*.lockb \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8d0a2d44 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,137 @@ + + +# Contributing + +Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute. + + +Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues. + +We have a few guidelines to follow when contributing to this project: + +- [Commit Convention](#commit-convention) +- [Setup](#setup) +- [Development](#development) +- [Build](#build) +- [Pull Request](#pull-request) + +## Commit Convention + +Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. + +### Commit Message Format +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +#### Type +Must be one of the following: + +* **feat**: A new feature +* **fix**: A bug fix +* **docs**: Documentation only changes +* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +* **refactor**: A code change that neither fixes a bug nor adds a feature +* **perf**: A code change that improves performance +* **test**: Adding missing tests or correcting existing tests +* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) +* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) +* **chore**: Other changes that don't modify `src` or `test` files +* **revert**: Reverts a previous commit + +Example: +``` +feat: add new feature +``` + + +## Setup + +```bash +git clone https://github.com/dokploy/dokploy.git +cd dokploy +npm install +cp .env.example .env +``` + +## Development + +Is required to have **Docker** installed on your machine. + +```bash +npm run dev +``` + +Go to http://localhost:3000 to see the development server + +## Build + +```bash +npm run build +``` + +## Docker + +To build the docker image +```bash +npm run docker:build +``` + +To push the docker image +```bash +npm run docker:push +``` + +## Password Reset + +In the case you lost your password, you can reset it using the following command + +```bash +pnpm run build-server +``` + +If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` + +```bash +bunx lt --port 3000 +``` + +If you run into permission issues of docker run the following command + +```bash +sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker +``` + +## Application deploy + +In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first. + +```bash +# Install Nixpacks +curl -sSL https://nixpacks.com/install.sh -o install.sh \ + && chmod +x install.sh \ + && ./install.sh +``` + +```bash +# Install Buildpacks +curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack +``` + + +## Pull Request + +- The `main` branch is the source of truth and should always reflect the latest stable release. +- Create a new branch for each feature or bug fix. +- Make sure to add tests for your changes. +- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. +- When creating a pull request, please provide a clear and concise description of the changes made. +- If you include a video or screenshot, would be awesome so we can see the changes in action. +- If your pull request fixes an open issue, please reference the issue in the pull request description. +- Once your pull request is merged, you will be automatically added as a contributor to the project. + +Thank you for your contribution! \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..97d7b26a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,65 @@ +# Etapa 1: Prepare image for building +FROM node:18-slim AS base + +# Install dependencies +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy package.json and pnpm-lock.yaml +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies only for building +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile + +# Copy the rest of the source code +COPY . . + +# Build the application +RUN pnpm run build + +# Stage 2: Prepare image for production +FROM node:18-slim AS production + +# Install dependencies only for production +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the rest of the source code +COPY --from=base /app/.next ./.next +COPY --from=base /app/dist ./dist +COPY --from=base /app/next.config.mjs ./next.config.mjs +COPY --from=base /app/public ./public +COPY --from=base /app/package.json ./package.json +COPY --from=base /app/drizzle ./drizzle +COPY --from=base /app/.env.production ./.env +COPY --from=base /app/components.json ./components.json + +# Install dependencies only for production +COPY package.json pnpm-lock.yaml ./ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +# Install docker +RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh + + +# Install Nixpacks and tsx +# | VERBOSE=1 VERSION=1.21.0 bash +RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ + && chmod +x install.sh \ + && ./install.sh \ + && pnpm install -g tsx + + +# Install buildpacks +RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack + +# Expose port +EXPOSE 3000 + +CMD ["pnpm", "start"] \ No newline at end of file diff --git a/LICENSE.MD b/LICENSE.MD new file mode 100644 index 00000000..abf659eb --- /dev/null +++ b/LICENSE.MD @@ -0,0 +1,35 @@ +Copyright 2024-2024 Mauricio Siu. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Appendix: +In case of a conflict, the terms of this appendix supersede the general apache license. + +- Unless provided with a written agreement or permission, the paid features of Dokploy (as a service) cannot be modified. +- Unless provided with a written agreement or permission, no persons are permitted to redistribute another paid version of Dokploy. +- Unless provided with a written agreement or permission, no persons are permitted to create the same paid version of Dokploy. +- Furthermore, any modifications of free features of Dokploy should be distributed as free & opensource software. + +Appendix B: + +- **Prohibition of Resale Without Permission:** Notwithstanding any provisions in the main body of the Apache License, Version 2.0, no party is permitted to sell, resell, or otherwise distribute for commercial gain, the software or any of its components, including both original and modified versions, through any form of commercial distribution channels, including but not limited to online marketplaces, software as a service (SaaS) platforms, or physical media distribution, without prior written consent from the copyright holder. + +- **Commercial Distribution:** Any form of distribution of Dokploy, whether for direct profit or indirect financial benefit, through commercial channels is strictly prohibited without a separate commercial agreement negotiated with the copyright holder. This includes but is not limited to, offerings on software marketplaces or through third-party distributors. + +- **Modification of Paid Features:** The paid features of Dokploy (as a service) may not be modified, integrated into other software, or redistributed in any form without explicit written permission from the copyright holder. + +- **Open Source Distribution of Free Features:** Any modifications to the free features of Dokploy must be distributed freely and must not be included in any paid or commercial package without complying with the open-source license terms stipulated in this agreement. + +If you have any questions, please feel free to reach out to us. + + diff --git a/README.md b/README.md index 68b792b0..19e44df0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
-Reflex Logo +Reflex Logo

@@ -42,3 +42,4 @@ Tested Systems: ## 📄 Documentation For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com). + diff --git a/TERMS_AND_CONDITIONS.md b/TERMS_AND_CONDITIONS.md new file mode 100644 index 00000000..acf376cd --- /dev/null +++ b/TERMS_AND_CONDITIONS.md @@ -0,0 +1,25 @@ +# Terms & Conditions + + +Dokploy core is a free and open-source program alternative to Vercel, Netlify, and other cloud services. + +Developers of Dokploy do their best to prevent bugs and issues through rigorous testing processes and clean code principles. Dokploy is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the [License](https://github.com/Dokploy/Dokploy/blob/main/LICENSE). + +By using Dokploy you agree to Terms and Conditions, and the license of Dokploy. + +### Description of Service: + +Dokploy core is an open-source program designed to streamline application deployment processes for personal and commercial use. Users are free to install, modify, and run Dokploy on their own machines or within their organizations to enhance their development and deployment workflows. While Dokploy encourages a wide range of uses to foster innovation and efficiency, it is crucial to note that selling Dokploy itself as a service or repackaging it as part of a commercial offering without explicit permission is strictly prohibited. This ensures that the open-source nature of Dokploy remains intact and benefits the community as a whole. + +### Our Responsibility + +Dokploy developers will do their best to ensure that Dokploy remains functional and major bugs are resolved quickly. If you have a feature request, you are more than welcome to open a request for it, but the ultimate decision whether or not the feature will be added is taken by Dokploy's core developers. + +### Usage Data + +Dokploy doesn't collect any usage data. It is a free and open-source program, and it is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +### Future changes + +Terms of Service / Terms & Conditions may change at any point without a prior notice. \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..dd7c1eb7 --- /dev/null +++ b/biome.json @@ -0,0 +1,17 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "linter":{ + "rules": { + "correctness":{ + "useExhaustiveDependencies": "off" + }, + "suspicious":{ + "noArrayIndexKey": "off" + }, + "a11y":{ + "noSvgWithoutTitle":"off" + } + } + } + +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 00000000..b4baac4f --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "styles/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/auth/login-2fa.tsx b/components/auth/login-2fa.tsx new file mode 100644 index 00000000..46d82cbe --- /dev/null +++ b/components/auth/login-2fa.tsx @@ -0,0 +1,124 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +import { CardTitle } from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/components/ui/input-otp"; +import { useRouter } from "next/router"; + +const Login2FASchema = z.object({ + pin: z.string().min(6, { + message: "Pin is required", + }), +}); + +type Login2FA = z.infer; + +interface Props { + authId: string; +} + +export const Login2FA = ({ authId }: Props) => { + const { push } = useRouter(); + + const { mutateAsync, isLoading, isError, error } = + api.auth.verifyLogin2FA.useMutation(); + + const form = useForm({ + defaultValues: { + pin: "", + }, + resolver: zodResolver(Login2FASchema), + }); + + useEffect(() => { + form.reset({ + pin: "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: Login2FA) => { + await mutateAsync({ + pin: data.pin, + id: authId, + }) + .then(() => { + toast.success("Signin succesfully", { + duration: 2000, + }); + + push("/dashboard"); + }) + .catch(() => { + toast.error("Signin failed", { + duration: 2000, + }); + }); + }; + return ( +
+ + {isError && ( +
+ + + {error?.message} + +
+ )} + 2FA Setup + + ( + + Pin + + + + + + + + + + + + + + Please enter the 6 digits code provided by your authenticator + app. + + + + )} + /> + + + + ); +}; diff --git a/components/dashboard/application/advanced/general/add-command.tsx b/components/dashboard/application/advanced/general/add-command.tsx new file mode 100644 index 00000000..b7de2ae6 --- /dev/null +++ b/components/dashboard/application/advanced/general/add-command.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { toast } from "sonner"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +interface Props { + applicationId: string; +} + +const AddRedirectchema = z.object({ + command: z.string(), +}); + +type AddCommand = z.infer; + +export const AddCommand = ({ applicationId }: Props) => { + const { data } = api.application.one.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + const utils = api.useUtils(); + + const { mutateAsync, isLoading } = api.application.update.useMutation(); + + const form = useForm({ + defaultValues: { + command: "", + }, + resolver: zodResolver(AddRedirectchema), + }); + + useEffect(() => { + if (data?.command) { + form.reset({ + command: data?.command || "", + }); + } + }, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]); + + const onSubmit = async (data: AddCommand) => { + await mutateAsync({ + applicationId, + command: data?.command, + }) + .then(async () => { + toast.success("Command Updated"); + await utils.application.one.invalidate({ + applicationId, + }); + }) + .catch(() => { + toast.error("Error to update the command"); + }); + }; + + return ( + + +
+ Run Command + + Run a custom command in the container + +
+
+ +
+ +
+ ( + + Command + + + + + + + )} + /> +
+
+ +
+
+ +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/ports/add-port.tsx b/components/dashboard/application/advanced/ports/add-port.tsx new file mode 100644 index 00000000..ceb9657c --- /dev/null +++ b/components/dashboard/application/advanced/ports/add-port.tsx @@ -0,0 +1,220 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { PlusIcon } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { z } from "zod"; + +const AddPortchema = z.object({ + publishedPort: z.number().int().min(1).max(65535), + targetPort: z.number().int().min(1).max(65535), + protocol: z.enum(["tcp", "udp"], { + required_error: "Protocol is required", + }), +}); + +type AddPort = z.infer; + +interface Props { + applicationId: string; + children?: React.ReactNode; +} + +export const AddPort = ({ + applicationId, + children = , +}: Props) => { + const utils = api.useUtils(); + + const { mutateAsync, isLoading, error, isError } = + api.port.create.useMutation(); + + const form = useForm({ + defaultValues: { + publishedPort: 0, + targetPort: 0, + }, + resolver: zodResolver(AddPortchema), + }); + + useEffect(() => { + form.reset({ + publishedPort: 0, + targetPort: 0, + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: AddPort) => { + await mutateAsync({ + applicationId, + ...data, + }) + .then(async () => { + toast.success("Port Created"); + await utils.application.one.invalidate({ + applicationId, + }); + }) + .catch(() => { + toast.error("Error to create the port"); + }); + }; + + return ( + + + + + + + Ports + + Ports are used to expose your application to the internet. + + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Published Port + + { + const value = e.target.value; + if (value === "") { + field.onChange(0); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> + + + + + )} + /> + ( + + Target Port + + { + const value = e.target.value; + if (value === "") { + field.onChange(0); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> + + + + + )} + /> + { + return ( + + Protocol + + + + ); + }} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/ports/delete-port.tsx b/components/dashboard/application/advanced/ports/delete-port.tsx new file mode 100644 index 00000000..cc2c7776 --- /dev/null +++ b/components/dashboard/application/advanced/ports/delete-port.tsx @@ -0,0 +1,63 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { TrashIcon } from "lucide-react"; +import { toast } from "sonner"; + +interface Props { + portId: string; +} + +export const DeletePort = ({ portId }: Props) => { + const utils = api.useUtils(); + const { mutateAsync, isLoading } = api.port.delete.useMutation(); + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the port + + + + Cancel + { + await mutateAsync({ + portId, + }) + .then((data) => { + utils.application.one.invalidate({ + applicationId: data?.applicationId, + }); + + toast.success("Port delete succesfully"); + }) + .catch(() => { + toast.error("Error to delete the port"); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/application/advanced/ports/show-port.tsx b/components/dashboard/application/advanced/ports/show-port.tsx new file mode 100644 index 00000000..6ecd4b41 --- /dev/null +++ b/components/dashboard/application/advanced/ports/show-port.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { Rss } from "lucide-react"; +import { AddPort } from "./add-port"; +import { DeletePort } from "./delete-port"; +import { UpdatePort } from "./update-port"; +interface Props { + applicationId: string; +} + +export const ShowPorts = ({ applicationId }: Props) => { + const { data } = api.application.one.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + return ( + + +
+ Ports + + the ports allows you to expose your application to the internet + +
+ + {data && data?.ports.length > 0 && ( + Add Port + )} +
+ + {data?.ports.length === 0 ? ( +
+ + + No ports configured + + Add Port +
+ ) : ( +
+
+ {data?.ports.map((port) => ( +
+
+
+
+ Published Port + + {port.publishedPort} + +
+
+ Target Port + + {port.targetPort} + +
+
+ Protocol + + {port.protocol.toUpperCase()} + +
+
+
+ + +
+
+
+ ))} +
+
+ )} +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/ports/update-port.tsx b/components/dashboard/application/advanced/ports/update-port.tsx new file mode 100644 index 00000000..8bee5a81 --- /dev/null +++ b/components/dashboard/application/advanced/ports/update-port.tsx @@ -0,0 +1,228 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, Pencil } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const UpdatePortSchema = z.object({ + publishedPort: z.number().int().min(1).max(65535), + targetPort: z.number().int().min(1).max(65535), + protocol: z.enum(["tcp", "udp"], { + required_error: "Protocol is required", + invalid_type_error: "Protocol must be a valid protocol", + }), +}); + +type UpdatePort = z.infer; + +interface Props { + portId: string; +} + +export const UpdatePort = ({ portId }: Props) => { + const utils = api.useUtils(); + const { data } = api.port.one.useQuery( + { + portId, + }, + { + enabled: !!portId, + }, + ); + + const { mutateAsync, isLoading, error, isError } = + api.port.update.useMutation(); + + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(UpdatePortSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + publishedPort: data.publishedPort, + targetPort: data.targetPort, + protocol: data.protocol, + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: UpdatePort) => { + await mutateAsync({ + portId, + publishedPort: data.publishedPort, + targetPort: data.targetPort, + protocol: data.protocol, + }) + .then(async (response) => { + toast.success("Port Updated"); + await utils.application.one.invalidate({ + applicationId: response?.applicationId, + }); + }) + .catch(() => { + toast.error("Error to update the port"); + }); + }; + + return ( + + + + + + + Update + Update the port + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Published Port + + { + const value = e.target.value; + if (value === "") { + field.onChange(0); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> + + + + + )} + /> + ( + + Target Port + + { + const value = e.target.value; + if (value === "") { + field.onChange(0); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> + + + + + )} + /> + { + return ( + + Protocol + + + + ); + }} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/redirects/add-redirect.tsx b/components/dashboard/application/advanced/redirects/add-redirect.tsx new file mode 100644 index 00000000..114564b7 --- /dev/null +++ b/components/dashboard/application/advanced/redirects/add-redirect.tsx @@ -0,0 +1,183 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormDescription, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { PlusIcon } from "lucide-react"; +import { z } from "zod"; +import { Switch } from "@/components/ui/switch"; + +const AddRedirectchema = z.object({ + regex: z.string().min(1, "Regex required"), + permanent: z.boolean().default(false), + replacement: z.string().min(1, "Replacement required"), +}); + +type AddRedirect = z.infer; + +interface Props { + applicationId: string; + children?: React.ReactNode; +} + +export const AddRedirect = ({ + applicationId, + children = , +}: Props) => { + const utils = api.useUtils(); + + const { mutateAsync, isLoading, error, isError } = + api.redirects.create.useMutation(); + + const form = useForm({ + defaultValues: { + permanent: false, + regex: "", + replacement: "", + }, + resolver: zodResolver(AddRedirectchema), + }); + + useEffect(() => { + form.reset({ + permanent: false, + regex: "", + replacement: "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: AddRedirect) => { + await mutateAsync({ + applicationId, + ...data, + }) + .then(async () => { + toast.success("Redirect Created"); + await utils.application.one.invalidate({ + applicationId, + }); + await utils.application.readTraefikConfig.invalidate({ + applicationId, + }); + }) + .catch(() => { + toast.error("Error to create the redirect"); + }); + }; + + return ( + + + + + + + Redirects + + Redirects are used to redirect requests to another url. + + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Regex + + + + + + + )} + /> + ( + + Replacement + + + + + + + )} + /> + + ( + +
+ Permanent + + Set the permanent option to true to apply a permanent + redirection. + +
+ + + +
+ )} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/redirects/delete-redirect.tsx b/components/dashboard/application/advanced/redirects/delete-redirect.tsx new file mode 100644 index 00000000..08d1f3e0 --- /dev/null +++ b/components/dashboard/application/advanced/redirects/delete-redirect.tsx @@ -0,0 +1,66 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { TrashIcon } from "lucide-react"; +import { toast } from "sonner"; + +interface Props { + redirectId: string; +} + +export const DeleteRedirect = ({ redirectId }: Props) => { + const utils = api.useUtils(); + const { mutateAsync, isLoading } = api.redirects.delete.useMutation(); + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + redirect + + + + Cancel + { + await mutateAsync({ + redirectId, + }) + .then((data) => { + utils.application.one.invalidate({ + applicationId: data?.applicationId, + }); + utils.application.readTraefikConfig.invalidate({ + applicationId: data?.applicationId, + }); + toast.success("Redirect delete succesfully"); + }) + .catch(() => { + toast.error("Error to delete the redirect"); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/application/advanced/redirects/show-redirects.tsx b/components/dashboard/application/advanced/redirects/show-redirects.tsx new file mode 100644 index 00000000..2a6e80a2 --- /dev/null +++ b/components/dashboard/application/advanced/redirects/show-redirects.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { Split } from "lucide-react"; +import { AddRedirect } from "./add-redirect"; +import { DeleteRedirect } from "./delete-redirect"; +import { UpdateRedirect } from "./update-redirect"; +interface Props { + applicationId: string; +} + +export const ShowRedirects = ({ applicationId }: Props) => { + const { data } = api.application.one.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + return ( + + +
+ Redirects + + If you want to redirect requests to this application use the + following config to setup the redirects + +
+ + {data && data?.redirects.length > 0 && ( + Add Redirect + )} +
+ + {data?.redirects.length === 0 ? ( +
+ + + No redirects configured + + + Add Redirect + +
+ ) : ( +
+
+ {data?.redirects.map((redirect) => ( +
+
+
+
+ Regex + + {redirect.regex} + +
+
+ Replacement + + {redirect.replacement} + +
+
+ Permanent + + {redirect.permanent ? "Yes" : "No"} + +
+
+
+ + +
+
+
+ ))} +
+
+ )} +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/redirects/update-redirect.tsx b/components/dashboard/application/advanced/redirects/update-redirect.tsx new file mode 100644 index 00000000..5b0cd347 --- /dev/null +++ b/components/dashboard/application/advanced/redirects/update-redirect.tsx @@ -0,0 +1,186 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormDescription, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, Pencil } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Switch } from "@/components/ui/switch"; +const UpdateRedirectSchema = z.object({ + regex: z.string().min(1, "Regex required"), + permanent: z.boolean().default(false), + replacement: z.string().min(1, "Replacement required"), +}); + +type UpdateRedirect = z.infer; + +interface Props { + redirectId: string; +} + +export const UpdateRedirect = ({ redirectId }: Props) => { + const utils = api.useUtils(); + const { data } = api.redirects.one.useQuery( + { + redirectId, + }, + { + enabled: !!redirectId, + }, + ); + + const { mutateAsync, isLoading, error, isError } = + api.redirects.update.useMutation(); + + const form = useForm({ + defaultValues: { + permanent: false, + regex: "", + replacement: "", + }, + resolver: zodResolver(UpdateRedirectSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + permanent: data.permanent || false, + regex: data.regex || "", + replacement: data.replacement || "", + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: UpdateRedirect) => { + await mutateAsync({ + redirectId, + permanent: data.permanent, + regex: data.regex, + replacement: data.replacement, + }) + .then(async (response) => { + toast.success("Redirect Updated"); + await utils.application.one.invalidate({ + applicationId: response?.applicationId, + }); + }) + .catch(() => { + toast.error("Error to update the redirect"); + }); + }; + + return ( + + + + + + + Update + Update the redirect + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Regex + + + + + + + )} + /> + ( + + Replacement + + + + + + + )} + /> + + ( + +
+ Permanent + + Set the permanent option to true to apply a permanent + redirection. + +
+ + + +
+ )} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/security/add-security.tsx b/components/dashboard/application/advanced/security/add-security.tsx new file mode 100644 index 00000000..09dbfe0e --- /dev/null +++ b/components/dashboard/application/advanced/security/add-security.tsx @@ -0,0 +1,153 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { PlusIcon } from "lucide-react"; +import { z } from "zod"; + +const AddSecuritychema = z.object({ + username: z.string().min(1, "Username is required"), + password: z.string().min(1, "Password is required"), +}); + +type AddSecurity = z.infer; + +interface Props { + applicationId: string; + children?: React.ReactNode; +} + +export const AddSecurity = ({ + applicationId, + children = , +}: Props) => { + const utils = api.useUtils(); + + const { mutateAsync, isLoading, error, isError } = + api.security.create.useMutation(); + + const form = useForm({ + defaultValues: { + username: "", + password: "", + }, + resolver: zodResolver(AddSecuritychema), + }); + + useEffect(() => { + form.reset(); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: AddSecurity) => { + await mutateAsync({ + applicationId, + ...data, + }) + .then(async () => { + toast.success("Security Created"); + await utils.application.one.invalidate({ + applicationId, + }); + await utils.application.readTraefikConfig.invalidate({ + applicationId, + }); + }) + .catch(() => { + toast.error("Error to create the security"); + }); + }; + + return ( + + + + + + + Security + + Add security to your application + + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Username + + + + + + + )} + /> + ( + + Password + + + + + + + )} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/security/delete-security.tsx b/components/dashboard/application/advanced/security/delete-security.tsx new file mode 100644 index 00000000..1a63c234 --- /dev/null +++ b/components/dashboard/application/advanced/security/delete-security.tsx @@ -0,0 +1,66 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { TrashIcon } from "lucide-react"; +import { toast } from "sonner"; + +interface Props { + securityId: string; +} + +export const DeleteSecurity = ({ securityId }: Props) => { + const utils = api.useUtils(); + const { mutateAsync, isLoading } = api.security.delete.useMutation(); + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + security + + + + Cancel + { + await mutateAsync({ + securityId, + }) + .then((data) => { + utils.application.one.invalidate({ + applicationId: data?.applicationId, + }); + utils.application.readTraefikConfig.invalidate({ + applicationId: data?.applicationId, + }); + toast.success("Security delete succesfully"); + }) + .catch(() => { + toast.error("Error to delete the security"); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/application/advanced/security/show-security.tsx b/components/dashboard/application/advanced/security/show-security.tsx new file mode 100644 index 00000000..ef51e2c9 --- /dev/null +++ b/components/dashboard/application/advanced/security/show-security.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { LockKeyhole } from "lucide-react"; +import { AddSecurity } from "./add-security"; +import { DeleteSecurity } from "./delete-security"; +import { UpdateSecurity } from "./update-security"; +interface Props { + applicationId: string; +} + +export const ShowSecurity = ({ applicationId }: Props) => { + const { data } = api.application.one.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + return ( + + +
+ Security + Add basic auth to your application +
+ + {data && data?.security.length > 0 && ( + Add Security + )} +
+ + {data?.security.length === 0 ? ( +
+ + + No security configured + + + Add Security + +
+ ) : ( +
+
+ {data?.security.map((security) => ( +
+
+
+
+ Username + + {security.username} + +
+
+ Password + + {security.password} + +
+
+
+ + +
+
+
+ ))} +
+
+ )} +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/security/update-security.tsx b/components/dashboard/application/advanced/security/update-security.tsx new file mode 100644 index 00000000..72df0d55 --- /dev/null +++ b/components/dashboard/application/advanced/security/update-security.tsx @@ -0,0 +1,159 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, Pencil } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const UpdateSecuritySchema = z.object({ + username: z.string().min(1, "Username is required"), + password: z.string().min(1, "Password is required"), +}); + +type UpdateSecurity = z.infer; + +interface Props { + securityId: string; +} + +export const UpdateSecurity = ({ securityId }: Props) => { + const utils = api.useUtils(); + const { data } = api.security.one.useQuery( + { + securityId, + }, + { + enabled: !!securityId, + }, + ); + + const { mutateAsync, isLoading, error, isError } = + api.security.update.useMutation(); + + const form = useForm({ + defaultValues: { + username: "", + password: "", + }, + resolver: zodResolver(UpdateSecuritySchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + username: data.username || "", + password: data.password || "", + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: UpdateSecurity) => { + await mutateAsync({ + securityId, + username: data.username, + password: data.password, + }) + .then(async (response) => { + toast.success("Security Updated"); + await utils.application.one.invalidate({ + applicationId: response?.applicationId, + }); + }) + .catch(() => { + toast.error("Error to update the security"); + }); + }; + + return ( + + + + + + + Update + Update the security + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Username + + + + + + + )} + /> + ( + + Password + + + + + + + )} + /> +
+
+ + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/show-application-advanced-settings.tsx b/components/dashboard/application/advanced/show-application-advanced-settings.tsx new file mode 100644 index 00000000..be540dd2 --- /dev/null +++ b/components/dashboard/application/advanced/show-application-advanced-settings.tsx @@ -0,0 +1,226 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const addResourcesApplication = z.object({ + memoryReservation: z.number().nullable().optional(), + cpuLimit: z.number().nullable().optional(), + memoryLimit: z.number().nullable().optional(), + cpuReservation: z.number().nullable().optional(), +}); +interface Props { + applicationId: string; +} + +type AddResourcesApplication = z.infer; + +export const ShowApplicationResources = ({ applicationId }: Props) => { + const { data, refetch } = api.application.one.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + const { mutateAsync, isLoading } = api.application.update.useMutation(); + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(addResourcesApplication), + }); + + useEffect(() => { + if (data) { + form.reset({ + cpuLimit: data?.cpuLimit || undefined, + cpuReservation: data?.cpuReservation || undefined, + memoryLimit: data?.memoryLimit || undefined, + memoryReservation: data?.memoryReservation || undefined, + }); + } + }, [data, form, form.reset]); + + const onSubmit = async (formData: AddResourcesApplication) => { + await mutateAsync({ + applicationId, + cpuLimit: formData.cpuLimit || null, + cpuReservation: formData.cpuReservation || null, + memoryLimit: formData.memoryLimit || null, + memoryReservation: formData.memoryReservation || null, + }) + .then(async () => { + toast.success("Resources Updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error to Update the resources"); + }); + }; + return ( + + + Resources + + If you want to decrease or increase the resources to a specific + application or database + + + +
+ +
+ ( + + Memory Reservation + + { + const value = e.target.value; + if (value === "") { + field.onChange(null); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> + + + + + )} + /> + + { + return ( + + Memory Limit + + { + const value = e.target.value; + if (value === "") { + field.onChange(null); + } else { + const number = Number.parseInt(value, 10); + if (!Number.isNaN(number)) { + field.onChange(number); + } + } + }} + /> + + + + ); + }} + /> + + { + return ( + + Cpu Limit + + { + const value = e.target.value; + if ( + value === "" || + /^[0-9]*\.?[0-9]*$/.test(value) + ) { + const float = Number.parseFloat(value); + field.onChange(float); + } + }} + /> + + + + ); + }} + /> + { + return ( + + Cpu Reservation + + { + const value = e.target.value; + if ( + value === "" || + /^[0-9]*\.?[0-9]*$/.test(value) + ) { + const float = Number.parseFloat(value); + field.onChange(float); + } + }} + /> + + + + ); + }} + /> +
+
+ +
+
+ +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/traefik/show-traefik-config.tsx b/components/dashboard/application/advanced/traefik/show-traefik-config.tsx new file mode 100644 index 00000000..7a87e4e8 --- /dev/null +++ b/components/dashboard/application/advanced/traefik/show-traefik-config.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { File } from "lucide-react"; +import { UpdateTraefikConfig } from "./update-traefik-config"; +interface Props { + applicationId: string; +} + +export const ShowTraefikConfig = ({ applicationId }: Props) => { + const { data } = api.application.readTraefikConfig.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + return ( + + +
+ Traefik + + Modify the traefik config, in rare cases you may need to add + specific config, becarefull because modifying incorrectly can break + traefik and your application + +
+
+ + {data === null ? ( +
+ + + No traefik config detected + +
+ ) : ( +
+
+
+
{data || "Empty"}
+
+
+ +
+
+
+ )} +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/components/dashboard/application/advanced/traefik/update-traefik-config.tsx new file mode 100644 index 00000000..f4422cd8 --- /dev/null +++ b/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -0,0 +1,180 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import jsyaml from "js-yaml"; + +const UpdateTraefikConfigSchema = z.object({ + traefikConfig: z.string(), +}); + +type UpdateTraefikConfig = z.infer; + +interface Props { + applicationId: string; +} + +export const validateAndFormatYAML = (yamlText: string) => { + try { + const obj = jsyaml.load(yamlText); + const formattedYaml = jsyaml.dump(obj, { indent: 4 }); + return { valid: true, formattedYaml, error: null }; + } catch (error) { + if (error instanceof jsyaml.YAMLException) { + return { + valid: false, + formattedYaml: yamlText, + error: error.message, + }; + } + return { + valid: false, + formattedYaml: yamlText, + error: "An unexpected error occurred while processing the YAML.", + }; + } +}; + +export const UpdateTraefikConfig = ({ applicationId }: Props) => { + const { data, refetch } = api.application.readTraefikConfig.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + const { mutateAsync, isLoading, error, isError } = + api.application.updateTraefikConfig.useMutation(); + + const form = useForm({ + defaultValues: { + traefikConfig: "", + }, + resolver: zodResolver(UpdateTraefikConfigSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + traefikConfig: data || "", + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: UpdateTraefikConfig) => { + const { valid, error } = validateAndFormatYAML(data.traefikConfig); + if (!valid) { + form.setError("traefikConfig", { + type: "manual", + message: error || "Invalid YAML", + }); + return; + } + form.clearErrors("traefikConfig"); + await mutateAsync({ + applicationId, + traefikConfig: data.traefikConfig, + }) + .then(async () => { + toast.success("Traefik config Updated"); + refetch(); + }) + .catch(() => { + toast.error("Error to update the traefik config"); + }); + }; + + return ( + + + + + + + Update traefik config + Update the traefik config + + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+ +
+ ( + + Traefik config + +