)}
@@ -252,6 +259,8 @@ export const ShowDeployments = ({
const isExpanded = expandedDescriptions.has(
deployment.deploymentId,
);
+ const canDelete =
+ deployment.status === "done" || deployment.status === "error";
return (
+ {canDelete && (
+
{
+ try {
+ await removeDeployment({
+ deploymentId: deployment.deploymentId,
+ });
+ toast.success("Deployment deleted successfully");
+ } catch (error) {
+ toast.error("Error deleting deployment");
+ }
+ }}
+ >
+
+ Delete
+
+
+
+ )}
+
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
index 6af0e1e8c..00eb62272 100644
--- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -159,11 +159,11 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
},
);
- const { mutateAsync, isError, error, isLoading } = domainId
+ const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
- const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
+ const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { data: canGenerateTraefikMeDomains } =
@@ -240,7 +240,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
domainType: type,
});
}
- }, [form, data, isLoading, domainId]);
+ }, [form, data, isPending, domainId]);
// Separate effect for handling custom cert resolver validation
useEffect(() => {
@@ -730,7 +730,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
-
+
{dictionary.submit}
diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
index 1fd3d82e9..c207ba59c 100644
--- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
@@ -97,7 +97,7 @@ export const ShowDomains = ({ id, type }: Props) => {
const { mutateAsync: validateDomain } =
api.domain.validateDomain.useMutation();
- const { mutateAsync: deleteDomain, isLoading: isRemoving } =
+ const { mutateAsync: deleteDomain, isPending: isRemoving } =
api.domain.delete.useMutation();
const handleValidateDomain = async (host: string) => {
diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
index 797a317a8..8ff0f6a63 100644
--- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
+++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -60,7 +60,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
compose: () => api.compose.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -111,7 +111,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -121,7 +121,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -196,7 +196,7 @@ PORT=3000
)}
{
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveEnvironment.useMutation();
const { data, refetch } = api.application.one.useQuery(
@@ -104,7 +104,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -114,7 +114,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -214,7 +214,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
)}
{
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingBitbucketProvider } =
+ const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.application.saveBitbucketProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -245,13 +245,13 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -263,11 +263,15 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!bitbucketId ? (
+
+ Select a Bitbucket account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -329,7 +333,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -346,7 +350,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx
index fcdcf0a93..078271bca 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx
index 00e18c2ab..583b865c5 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { TrashIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -24,10 +24,10 @@ interface Props {
export const SaveDragNDrop = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.dropDeployment.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(uploadFileSchema),
});
@@ -129,8 +129,8 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
Deploy{" "}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx
index e9be3a2f5..624adeb55 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,10 +58,10 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.application.saveGitProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
branch: "",
buildPath: "/",
@@ -317,7 +317,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
-
+
Save
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx
index 2198f4a97..02cae2c4a 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -88,10 +88,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGiteaProvider } =
+ const { mutateAsync, isPending: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
- const form = useForm
({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -258,14 +258,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -277,11 +277,15 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!giteaId ? (
+
+ Select a Gitea account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -349,7 +353,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -367,7 +371,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -459,7 +463,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
{
- const newPaths = [...field.value];
+ const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
@@ -477,7 +481,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
- field.onChange([...field.value, path]);
+ field.onChange([...(field.value || []), path]);
input.value = "";
}
}
@@ -494,7 +498,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
- field.onChange([...field.value, path]);
+ field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
index 80d6850ca..6bce2d243 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGithubProvider } =
+ const { mutateAsync, isPending: isSavingGithubProvider } =
api.application.saveGithubProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
- const { data: repositories, isLoading: isLoadingRepositories } =
+ const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -233,13 +233,13 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -251,11 +251,15 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!githubId ? (
+
+ Select a GitHub account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -316,7 +320,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -333,7 +337,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -455,7 +459,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => (
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx
index 6197fc49f..b49a1658f 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
- const { mutateAsync, isLoading: isSavingGitlabProvider } =
+ const { mutateAsync, isPending: isSavingGitlabProvider } =
api.application.saveGitlabProvider.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -254,13 +254,13 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -272,11 +272,15 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!gitlabId ? (
+
+ Select a GitLab account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -347,7 +351,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -364,7 +368,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -444,7 +448,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => (
diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx
index a60db800c..9a49b204e 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx
@@ -36,13 +36,13 @@ interface Props {
}
export const ShowProviderForm = ({ applicationId }: Props) => {
- const { data: githubProviders, isLoading: isLoadingGithub } =
+ const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
- const { data: gitlabProviders, isLoading: isLoadingGitlab } =
+ const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
- const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
+ const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
- const { data: giteaProviders, isLoading: isLoadingGitea } =
+ const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { data: application, refetch } = api.application.one.useQuery({
diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx
index 5387659ad..ee42caa5e 100644
--- a/apps/dokploy/components/dashboard/application/general/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/show.tsx
@@ -37,14 +37,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: update } = api.application.update.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.application.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.application.stop.useMutation();
const { mutateAsync: deploy } = api.application.deploy.useMutation();
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.application.reload.useMutation();
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx
index e5dff075e..cbb6bce09 100644
--- a/apps/dokploy/components/dashboard/application/logs/show.tsx
+++ b/apps/dokploy/components/dashboard/application/logs/show.tsx
@@ -34,6 +34,7 @@ export const DockerLogs = dynamic(
export const badgeStateColor = (state: string) => {
switch (state) {
case "running":
+ case "ready":
return "green";
case "exited":
case "shutdown":
@@ -55,7 +56,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const [containerId, setContainerId] = useState();
const [option, setOption] = useState<"swarm" | "native">("native");
- const { data: services, isLoading: servicesLoading } =
+ const { data: services, isPending: servicesLoading } =
api.docker.getServiceContainersByAppName.useQuery(
{
appName,
@@ -66,7 +67,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
},
);
- const { data: containers, isLoading: containersLoading } =
+ const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
@@ -142,6 +143,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
{container.state}
+ {container.status ? ` ${container.status}` : ""}
))}
@@ -157,6 +159,9 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
{container.state}
+ {container.currentState
+ ? ` ${container.currentState}`
+ : ""}
))}
>
@@ -166,6 +171,13 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
+ {option === "swarm" &&
+ services?.find((c) => c.containerId === containerId)?.error && (
+
+ Error:
+ {services?.find((c) => c.containerId === containerId)?.error}
+
+ )}
void;
+ onOpenChange: (open: boolean) => void;
+ alwaysVisible?: boolean;
+}
+
+export const CreateFileDialog = ({
+ folderPath,
+ onCreate,
+ onOpenChange,
+ alwaysVisible = false,
+}: Props) => {
+ const [filename, setFilename] = useState("");
+ const [content, setContent] = useState("");
+
+ const handleCreate = () => {
+ if (!filename.trim()) return;
+ onCreate(filename.trim(), content);
+ setFilename("");
+ setContent("");
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx b/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx
new file mode 100644
index 000000000..8c5a42836
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/edit-patch-dialog.tsx
@@ -0,0 +1,102 @@
+import { Loader2, Pencil } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { api } from "@/utils/api";
+
+interface Props {
+ patchId: string;
+ entityId: string;
+ type: "application" | "compose";
+ onSuccess?: () => void;
+}
+
+export const EditPatchDialog = ({
+ patchId,
+ entityId,
+ type,
+ onSuccess,
+}: Props) => {
+ const { data: patch, isPending: isPatchLoading } = api.patch.one.useQuery(
+ { patchId },
+ { enabled: !!patchId },
+ );
+ const [content, setContent] = useState("");
+
+ useEffect(() => {
+ if (patch) {
+ setContent(patch.content);
+ }
+ }, [patch]);
+
+ const utils = api.useUtils();
+ const updatePatch = api.patch.update.useMutation();
+
+ const handleSave = () => {
+ updatePatch
+ .mutateAsync({ patchId, content })
+ .then(() => {
+ toast.success("Patch saved");
+ utils.patch.byEntityId.invalidate({ id: entityId, type });
+ onSuccess?.();
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Edit Patch
+
+ {patch ? `Editing: ${patch.filePath}` : "Loading patch..."}
+
+
+ {isPatchLoading ? (
+
+
+
+ ) : (
+
+ setContent(value ?? "")}
+ className="h-[400px] w-full"
+ wrapperClassName="h-[400px]"
+ lineWrapping
+ />
+
+ )}
+
+
+ Cancel
+
+
+ {updatePatch.isPending && (
+
+ )}
+ Save
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/index.ts b/apps/dokploy/components/dashboard/application/patches/index.ts
new file mode 100644
index 000000000..1854bd3e5
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/index.ts
@@ -0,0 +1,2 @@
+export * from "./show-patches";
+export * from "./patch-editor";
diff --git a/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx b/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx
new file mode 100644
index 000000000..4b212b004
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/patch-editor.tsx
@@ -0,0 +1,368 @@
+import {
+ ArrowLeft,
+ ChevronRight,
+ File,
+ Folder,
+ Loader2,
+ Save,
+ Trash2,
+} from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
+import { toast } from "sonner";
+import { CodeEditor } from "@/components/shared/code-editor";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { api } from "@/utils/api";
+import { CreateFileDialog } from "./create-file-dialog";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+ repoPath: string;
+ onClose: () => void;
+}
+
+type DirectoryEntry = {
+ name: string;
+ path: string;
+ type: "file" | "directory";
+ children?: DirectoryEntry[];
+};
+
+export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [fileContent, setFileContent] = useState("");
+ const [createFolderPath, setCreateFolderPath] = useState(null);
+ const [expandedFolders, setExpandedFolders] = useState>(
+ new Set(),
+ );
+
+ const utils = api.useUtils();
+ const { data: directories, isPending: isDirLoading } =
+ api.patch.readRepoDirectories.useQuery(
+ { id: id, type, repoPath },
+ { enabled: !!repoPath },
+ );
+
+ const { data: patches } = api.patch.byEntityId.useQuery(
+ { id, type },
+ { enabled: !!id },
+ );
+
+ const { mutateAsync: saveAsPatch, isPending: isSavingPatch } =
+ api.patch.saveFileAsPatch.useMutation();
+
+ const { mutateAsync: markForDeletion, isPending: isMarkingDeletion } =
+ api.patch.markFileForDeletion.useMutation();
+
+ const updatePatch = api.patch.update.useMutation();
+
+ const { data: fileData, isFetching: isFileLoading } =
+ api.patch.readRepoFile.useQuery(
+ {
+ id,
+ type,
+ filePath: selectedFile || "",
+ },
+ {
+ enabled: !!selectedFile,
+ },
+ );
+
+ useEffect(() => {
+ if (fileData !== undefined) {
+ setFileContent(fileData);
+ }
+ }, [fileData]);
+
+ const handleFileSelect = (filePath: string) => {
+ setSelectedFile(filePath);
+ };
+
+ const toggleFolder = (path: string) => {
+ setExpandedFolders((prev) => {
+ const next = new Set(prev);
+ if (next.has(path)) {
+ next.delete(path);
+ } else {
+ next.add(path);
+ }
+ return next;
+ });
+ };
+
+ const handleSave = () => {
+ if (!selectedFile) return;
+ saveAsPatch({
+ id,
+ type,
+ filePath: selectedFile,
+ content: fileContent,
+ patchType: "update",
+ })
+ .then(() => {
+ toast.success("Patch saved");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to save patch");
+ });
+ };
+
+ const handleMarkForDeletion = () => {
+ if (!selectedFile) return;
+ markForDeletion({ id, type, filePath: selectedFile })
+ .then(() => {
+ toast.success("File marked for deletion");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to mark file for deletion");
+ });
+ };
+
+ const handleCreateFile = useCallback(
+ (folderPath: string, filename: string, content: string) => {
+ const filePath = folderPath ? `${folderPath}/${filename}` : filename;
+ saveAsPatch({
+ id,
+ type,
+ filePath,
+ content,
+ patchType: "create",
+ })
+ .then(() => {
+ toast.success("File created");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to create file");
+ });
+ },
+ [id, type, saveAsPatch, utils],
+ );
+
+ const selectedFilePatch = patches?.find(
+ (p) => p.filePath === selectedFile && p.type === "delete",
+ );
+
+ const handleUnmarkDeletion = () => {
+ if (!selectedFilePatch) return;
+ updatePatch
+ .mutateAsync({
+ patchId: selectedFilePatch.patchId,
+ type: "update",
+ content: fileData || "",
+ })
+ .then(() => {
+ toast.success("Deletion unmarked");
+ utils.patch.byEntityId.invalidate({ id, type });
+ })
+ .catch(() => {
+ toast.error("Failed to unmark deletion");
+ });
+ };
+
+ const hasChanges = fileData !== undefined && fileContent !== fileData;
+
+ const renderTree = useCallback(
+ (entries: DirectoryEntry[], depth = 0) => {
+ return entries
+ .sort((a, b) => {
+ // Directories first, then alphabetically
+ if (a.type !== b.type) {
+ return a.type === "directory" ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ })
+ .map((entry) => {
+ const isExpanded = expandedFolders.has(entry.path);
+ const isSelected = selectedFile === entry.path;
+
+ if (entry.type === "directory") {
+ return (
+
+
+ toggleFolder(entry.path)}
+ className={
+ "flex-1 flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors text-left min-w-0"
+ }
+ style={{ paddingLeft: `${depth * 12 + 8}px` }}
+ >
+
+
+ {entry.name}
+
+
+ handleCreateFile(entry.path, filename, content)
+ }
+ onOpenChange={(open) =>
+ setCreateFolderPath(open ? entry.path : null)
+ }
+ />
+
+ {isExpanded && entry.children && (
+
{renderTree(entry.children, depth + 1)}
+ )}
+
+ );
+ }
+
+ const isMarkedForDeletion = patches?.some(
+ (p) => p.filePath === entry.path && p.type === "delete",
+ );
+
+ return (
+ handleFileSelect(entry.path)}
+ className={`w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors ${
+ isSelected ? "bg-muted" : ""
+ } ${isMarkedForDeletion ? "text-destructive" : ""}`}
+ style={{ paddingLeft: `${depth * 12 + 28}px` }}
+ >
+
+ {entry.name}
+ {isMarkedForDeletion && (
+
+ )}
+
+ );
+ });
+ },
+ [expandedFolders, selectedFile, patches, handleCreateFile],
+ );
+
+ return (
+
+
+
+
+
+
+
+ Edit File
+
+ {selectedFile
+ ? `Editing: ${selectedFile}`
+ : "Select a file from the tree to edit"}
+
+
+
+ {selectedFile && (
+
+ {selectedFilePatch ? (
+
+ {updatePatch.isPending && (
+
+ )}
+ Unmark deletion
+
+ ) : (
+ <>
+
+ {isMarkingDeletion && (
+
+ )}
+
+ Mark for deletion
+
+
+ {isSavingPatch && (
+
+ )}
+
+ Save Patch
+
+ >
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ handleCreateFile("", filename, content)
+ }
+ onOpenChange={(open) =>
+ setCreateFolderPath(open ? "" : null)
+ }
+ />
+
+ New file in root
+
+
+ {isDirLoading ? (
+
+
+
+ ) : directories ? (
+ renderTree(directories)
+ ) : (
+
+ No files found
+
+ )}
+
+
+
+
+ {isFileLoading ? (
+
+
+
+ ) : selectedFile ? (
+
setFileContent(value || "")}
+ className="h-full w-full"
+ wrapperClassName="h-full"
+ lineWrapping
+ />
+ ) : (
+
+ Select a file to edit
+
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/patches/show-patches.tsx b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx
new file mode 100644
index 000000000..e471b3fc1
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx
@@ -0,0 +1,225 @@
+import { File, FilePlus2, Loader2, Trash2 } from "lucide-react";
+import { useState } from "react";
+import { toast } from "sonner";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Switch } from "@/components/ui/switch";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { api } from "@/utils/api";
+import { EditPatchDialog } from "./edit-patch-dialog";
+import { PatchEditor } from "./patch-editor";
+
+interface Props {
+ id: string;
+ type: "application" | "compose";
+}
+
+export const ShowPatches = ({ id, type }: Props) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [repoPath, setRepoPath] = useState(null);
+ const [isLoadingRepo, setIsLoadingRepo] = useState(false);
+
+ const utils = api.useUtils();
+
+ const { data: patches, isPending: isPatchesLoading } =
+ api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id });
+
+ const mutationMap = {
+ application: () => api.patch.delete.useMutation(),
+ compose: () => api.patch.delete.useMutation(),
+ };
+
+ const ensureRepo = api.patch.ensureRepo.useMutation();
+
+ const togglePatch = api.patch.toggleEnabled.useMutation();
+
+ const { mutateAsync } = mutationMap[type]
+ ? mutationMap[type]()
+ : api.patch.delete.useMutation();
+
+ const handleCloseEditor = () => {
+ setSelectedFile(null);
+ setRepoPath(null);
+ };
+
+ if (repoPath) {
+ return (
+
+ );
+ }
+
+ const handleOpenEditor = async () => {
+ setIsLoadingRepo(true);
+ await ensureRepo
+ .mutateAsync({ id, type })
+ .then((result) => {
+ setRepoPath(result);
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ })
+ .finally(() => {
+ setIsLoadingRepo(false);
+ });
+ };
+
+ return (
+
+
+
+ Patches
+
+ Apply code patches to your repository during build. Patches are
+ applied after cloning the repository and before building.
+
+
+ {patches && patches?.length > 0 && (
+
+ {isLoadingRepo && }
+
+ Create Patch
+
+ )}
+
+
+ {isPatchesLoading ? (
+
+
+
+ ) : patches?.length === 0 ? (
+
+
+
+
+
+
No patches yet
+
+ Add file patches to modify your repo before each build—configs,
+ env, or code. Create your first patch to get started.
+
+
+
+ {isLoadingRepo && (
+
+ )}
+
+ Create Patch
+
+
+ ) : (
+
+
+
+ File Path
+ Type
+ Enabled
+ Actions
+
+
+
+ {patches?.map((patch) => (
+
+
+
+
+ {patch.filePath}
+
+
+
+
+ {patch.type}
+
+
+
+ {
+ togglePatch
+ .mutateAsync({
+ patchId: patch.patchId,
+ enabled: checked,
+ })
+ .then(() => {
+ toast.success("Patch updated");
+ utils.patch.byEntityId.invalidate({
+ id,
+ type,
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ })
+ .finally(() => {
+ setIsLoadingRepo(false);
+ });
+ }}
+ />
+
+
+
+ {(patch.type === "update" || patch.type === "create") && (
+
+ )}
+ {
+ mutateAsync({ patchId: patch.patchId })
+ .then(() => {
+ toast.success("Patch deleted");
+ utils.patch.byEntityId.invalidate({
+ id,
+ type,
+ });
+ })
+ .catch((err) => {
+ toast.error(err.message);
+ });
+ }}
+ title="Delete patch"
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx
index bb9321a51..72815fd8f 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Dices } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -75,11 +75,11 @@ export const AddPreviewDomain = ({
},
);
- const { mutateAsync, isError, error, isLoading } = domainId
+ const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
- const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
+ const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm({
@@ -103,7 +103,7 @@ export const AddPreviewDomain = ({
if (!domainId) {
form.reset({});
}
- }, [form, form.reset, data, isLoading]);
+ }, [form, form.reset, data, isPending]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
@@ -301,7 +301,7 @@ export const AddPreviewDomain = ({
-
+
{dictionary.submit}
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
index 6cf8d8830..e12400a7c 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
@@ -43,7 +43,7 @@ interface Props {
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery({ applicationId });
- const { mutateAsync: deletePreviewDeployment, isLoading } =
+ const { mutateAsync: deletePreviewDeployment, isPending } =
api.previewDeployment.delete.useMutation();
const { mutateAsync: redeployPreviewDeployment } =
@@ -57,8 +57,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
- refetchInterval: (data) =>
- data?.some((d) => d.previewStatus === "running") ? 2000 : false,
+ refetchInterval: 2000,
},
);
@@ -282,7 +281,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx
index f8e6fab68..d2840cd67 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -80,7 +80,7 @@ interface Props {
export const ShowPreviewSettings = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
- const { mutateAsync: updateApplication, isLoading } =
+ const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data, refetch } = api.application.one.useQuery({ applicationId });
@@ -535,7 +535,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
Cancel
diff --git a/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx b/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
index a06cf5697..b119aa778 100644
--- a/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
+++ b/apps/dokploy/components/dashboard/application/rollbacks/show-rollback-settings.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -71,7 +71,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
},
);
- const { mutateAsync: updateApplication, isLoading } =
+ const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data: registries } = api.registry.all.useQuery();
@@ -212,7 +212,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
/>
)}
-
+
Save Settings
diff --git a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
index e85b1b004..36ddb53f1 100644
--- a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
CheckIcon,
ChevronsUpDown,
@@ -220,8 +220,8 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState("cache");
const utils = api.useUtils();
- const form = useForm>({
- resolver: zodResolver(formSchema),
+ const form = useForm({
+ resolver: standardSchemaResolver(formSchema),
defaultValues: {
name: "",
cronExpression: "",
@@ -275,11 +275,11 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
}
}, [form, schedule, scheduleId]);
- const { mutateAsync, isLoading } = scheduleId
+ const { mutateAsync, isPending } = scheduleId
? api.schedule.update.useMutation()
: api.schedule.create.useMutation();
- const onSubmit = async (values: z.infer) => {
+ const onSubmit = async (values: z.output) => {
if (!id && !scheduleId) return;
await mutateAsync({
@@ -662,7 +662,7 @@ echo "Hello, world!"
)}
/>
-
+
{scheduleId ? "Update" : "Create"} Schedule
diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
index 26bfa9421..a9550fda2 100644
--- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
@@ -51,7 +51,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
},
);
const utils = api.useUtils();
- const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
+ const { mutateAsync: deleteSchedule, isPending: isDeleting } =
api.schedule.delete.useMutation();
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();
diff --git a/apps/dokploy/components/dashboard/application/update-application.tsx b/apps/dokploy/components/dashboard/application/update-application.tsx
index 754074d75..98c49a999 100644
--- a/apps/dokploy/components/dashboard/application/update-application.tsx
+++ b/apps/dokploy/components/dashboard/application/update-application.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.application.update.useMutation();
const { data } = api.application.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
/>
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx
index e179713de..d0df60098 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -116,7 +116,7 @@ export const HandleVolumeBackups = ({
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
const utils = api.useUtils();
- const form = useForm>({
+ const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
@@ -195,7 +195,7 @@ export const HandleVolumeBackups = ({
}
}, [form, volumeBackup, volumeBackupId]);
- const { mutateAsync, isLoading } = volumeBackupId
+ const { mutateAsync, isPending } = volumeBackupId
? api.volumeBackups.update.useMutation()
: api.volumeBackups.create.useMutation();
@@ -207,7 +207,7 @@ export const HandleVolumeBackups = ({
await mutateAsync({
...values,
- keepLatestCount: preparedKeepLatestCount,
+ keepLatestCount: preparedKeepLatestCount ?? undefined,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
@@ -630,7 +630,7 @@ export const HandleVolumeBackups = ({
)}
/>
-
+
{volumeBackupId ? "Update" : "Create"} Volume Backup
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
index 6eda33648..684620947 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
-import { debounce } from "lodash";
+import debounce from "lodash/debounce";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -53,27 +53,15 @@ interface Props {
}
const RestoreBackupSchema = z.object({
- destinationId: z
- .string({
- required_error: "Please select a destination",
- })
- .min(1, {
- message: "Destination is required",
- }),
- backupFile: z
- .string({
- required_error: "Please select a backup file",
- })
- .min(1, {
- message: "Backup file is required",
- }),
- volumeName: z
- .string({
- required_error: "Please enter a volume name",
- })
- .min(1, {
- message: "Volume name is required",
- }),
+ destinationId: z.string().min(1, {
+ message: "Destination is required",
+ }),
+ backupFile: z.string().min(1, {
+ message: "Backup file is required",
+ }),
+ volumeName: z.string().min(1, {
+ message: "Volume name is required",
+ }),
});
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
@@ -83,7 +71,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const { data: destinations = [] } = api.destination.all.useQuery();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
destinationId: "",
backupFile: "",
@@ -105,7 +93,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
debouncedSetSearch(value);
};
- const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
+ const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destinationId,
search: debouncedSearchTerm,
@@ -294,7 +282,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
onValueChange={handleSearchChange}
className="h-9"
/>
- {isLoading ? (
+ {isPending ? (
Loading backup files...
diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
index 2e4dac472..526bcfa77 100644
--- a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
+++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx
@@ -54,7 +54,7 @@ export const ShowVolumeBackups = ({
},
);
const utils = api.useUtils();
- const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
+ const { mutateAsync: deleteVolumeBackup, isPending: isDeleting } =
api.volumeBackups.delete.useMutation();
const { mutateAsync: runManually } =
api.volumeBackups.runManually.useMutation();
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
index 52eb18907..c5f9334ec 100644
--- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
+++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -52,7 +52,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
const utils = api.useUtils();
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
const form = useForm({
defaultValues: {
@@ -128,7 +128,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
/>
-
+
Save
diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
index 5b6e04154..0fad7d20e 100644
--- a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
+++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx
index 5c8577dff..9d417ee91 100644
--- a/apps/dokploy/components/dashboard/compose/delete-service.tsx
+++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx
@@ -1,5 +1,5 @@
import type { ServiceType } from "@dokploy/server/db/schema";
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
@@ -74,7 +74,7 @@ export const DeleteService = ({ id, type }: Props) => {
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
@@ -130,7 +130,7 @@ export const DeleteService = ({ id, type }: Props) => {
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
- isLoading={isLoading}
+ isLoading={isPending}
>
@@ -228,7 +228,7 @@ export const DeleteService = ({ id, type }: Props) => {
{
const { mutateAsync: update } = api.compose.update.useMutation();
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.compose.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.compose.stop.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
index cb727e2a9..8193ec8b6 100644
--- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -34,7 +34,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
{ enabled: !!composeId },
);
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm
({
@@ -93,7 +93,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -103,7 +103,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
<>
@@ -167,7 +167,7 @@ services:
Save
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
index c89b9893e..3e099251e 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -74,10 +74,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingBitbucketProvider } =
+ const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -247,13 +247,13 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -265,11 +265,15 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!bitbucketId ? (
+
+ Select a Bitbucket account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -331,7 +335,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -348,7 +352,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
index d8c9d4d8f..4ad4f741c 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,9 +58,9 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
- const { mutateAsync, isLoading } = api.compose.update.useMutation();
+ const { mutateAsync, isPending } = api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
branch: "",
repositoryURL: "",
@@ -318,7 +318,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
-
+
Save{" "}
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
index fce562285..39f025438 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ interface Props {
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGiteaProvider } =
+ const { mutateAsync, isPending: isSavingGiteaProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -244,13 +244,13 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -261,11 +261,15 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!giteaId ? (
+
+ Select a Gitea account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -327,7 +331,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
index 5ad950e4c..827ce1a8a 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGithubProvider } =
+ const { mutateAsync, isPending: isSavingGithubProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository");
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
- const { data: repositories, isLoading: isLoadingRepositories } =
+ const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -234,13 +234,13 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -252,11 +252,15 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!githubId ? (
+
+ Select a GitHub account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -317,7 +321,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -334,7 +338,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
index 98c2afa11..63de87d8f 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
- const { mutateAsync, isLoading: isSavingGitlabProvider } =
+ const { mutateAsync, isPending: isSavingGitlabProvider } =
api.compose.update.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -256,13 +256,13 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {isLoadingRepositories
- ? "Loading...."
- : field.value.owner
- ? repositories?.find(
+ {!field.value.owner
+ ? "Select repository"
+ : isLoadingRepositories
+ ? "Loading...."
+ : (repositories?.find(
(repo) => repo.name === field.value.repo,
- )?.name
- : "Select repository"}
+ )?.name ?? "Select repository")}
@@ -274,11 +274,15 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
- {isLoadingRepositories && (
+ {!gitlabId ? (
+
+ Select a GitLab account first
+
+ ) : isLoadingRepositories ? (
Loading Repositories....
- )}
+ ) : null}
No repositories found.
@@ -349,7 +353,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
- {status === "loading" && fetchStatus === "fetching"
+ {status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -366,7 +370,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
index 798f72249..759fe728c 100644
--- a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx
@@ -27,13 +27,13 @@ interface Props {
}
export const ShowProviderFormCompose = ({ composeId }: Props) => {
- const { data: githubProviders, isLoading: isLoadingGithub } =
+ const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
- const { data: gitlabProviders, isLoading: isLoadingGitlab } =
+ const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
- const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
+ const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
- const { data: giteaProviders, isLoading: isLoadingGitea } =
+ const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { mutateAsync: disconnectGitProvider } =
diff --git a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
index 2c488aefe..99c749c26 100644
--- a/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/randomize-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
index fac6c2a34..211f5f5c7 100644
--- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx
@@ -32,7 +32,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
},
);
- const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
+ const { mutateAsync, isPending } = api.compose.fetchSourceType.useMutation();
useEffect(() => {
if (isOpen) {
@@ -66,7 +66,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
- {isLoading ? (
+ {isPending ? (
@@ -82,7 +82,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
{
mutateAsync({ composeId })
.then(() => {
diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
index 98c6c0470..159ab3485 100644
--- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
+++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
@@ -41,7 +41,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState();
- const { data: services, isLoading: servicesLoading } =
+ const { data: services, isPending: servicesLoading } =
api.docker.getStackContainersByAppName.useQuery(
{
appName,
@@ -52,7 +52,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
},
);
- const { data: containers, isLoading: containersLoading } =
+ const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
@@ -128,6 +128,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
{container.state}
+ {container.status ? ` ${container.status}` : ""}
))}
@@ -143,6 +144,9 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
{container.state}
+ {container.currentState
+ ? ` ${container.currentState}`
+ : ""}
))}
>
@@ -152,6 +156,13 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
+ {option === "swarm" &&
+ services?.find((c) => c.containerId === containerId)?.error && (
+
+ Error:
+ {services.find((c) => c.containerId === containerId)?.error}
+
+ )}
{
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
@@ -73,7 +73,7 @@ export const ShowDockerLogsCompose = ({
Select a container to view logs
- {isLoading ? (
+ {isPending ? (
Loading...
@@ -93,6 +93,7 @@ export const ShowDockerLogsCompose = ({
{container.state}
+ {container.status ? ` ${container.status}` : ""}
))}
Containers ({data?.length})
diff --git a/apps/dokploy/components/dashboard/compose/update-compose.tsx b/apps/dokploy/components/dashboard/compose/update-compose.tsx
index 7564988e2..91e04950a 100644
--- a/apps/dokploy/components/dashboard/compose/update-compose.tsx
+++ b/apps/dokploy/components/dashboard/compose/update-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.compose.update.useMutation();
const { data } = api.compose.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
/>
diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx
index f2ca41b85..3ef31c26f 100644
--- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import {
CheckIcon,
ChevronsUpDown,
@@ -192,7 +192,7 @@ export const HandleBackup = ({
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
- const { data, isLoading } = api.destination.all.useQuery();
+ const { data, isPending } = api.destination.all.useQuery();
const { data: backup } = api.backup.one.useQuery(
{
backupId: backupId ?? "",
@@ -202,12 +202,12 @@ export const HandleBackup = ({
},
);
const [cacheType, setCacheType] = useState("cache");
- const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } =
+ const { mutateAsync: createBackup, isPending: isCreatingPostgresBackup } =
backupId
? api.backup.update.useMutation()
: api.backup.create.useMutation();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
database: databaseType === "web-server" ? "dokploy" : "",
destinationId: "",
@@ -396,7 +396,7 @@ export const HandleBackup = ({
!field.value && "text-muted-foreground",
)}
>
- {isLoading
+ {isPending
? "Loading...."
: field.value
? data?.find(
@@ -415,7 +415,7 @@ export const HandleBackup = ({
placeholder="Search Destination..."
className="h-9"
/>
- {isLoading && (
+ {isPending && (
Loading Destinations....
@@ -613,6 +613,7 @@ export const HandleBackup = ({
type="number"
placeholder={"keeps all the backups if left empty"}
{...field}
+ value={field.value as string}
/>
diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
index 01f6944e1..ba8e4caf5 100644
--- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
-import _ from "lodash";
+import debounce from "lodash/debounce";
import {
CheckIcon,
ChevronsUpDown,
@@ -78,27 +78,15 @@ interface Props {
const RestoreBackupSchema = z
.object({
- destinationId: z
- .string({
- required_error: "Please select a destination",
- })
- .min(1, {
- message: "Destination is required",
- }),
- backupFile: z
- .string({
- required_error: "Please select a backup file",
- })
- .min(1, {
- message: "Backup file is required",
- }),
- databaseName: z
- .string({
- required_error: "Please enter a database name",
- })
- .min(1, {
- message: "Database name is required",
- }),
+ destinationId: z.string().min(1, {
+ message: "Destination is required",
+ }),
+ backupFile: z.string().min(1, {
+ message: "Backup file is required",
+ }),
+ databaseName: z.string().min(1, {
+ message: "Database name is required",
+ }),
databaseType: z
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
.optional(),
@@ -219,7 +207,7 @@ export const RestoreBackup = ({
const { data: destinations = [] } = api.destination.all.useQuery();
- const form = useForm>({
+ const form = useForm({
defaultValues: {
destinationId: "",
backupFile: "",
@@ -236,7 +224,7 @@ export const RestoreBackup = ({
const currentDatabaseType = form.watch("databaseType");
const metadata = form.watch("metadata");
- const debouncedSetSearch = _.debounce((value: string) => {
+ const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 350);
@@ -245,7 +233,7 @@ export const RestoreBackup = ({
debouncedSetSearch(value);
};
- const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
+ const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destionationId,
search: debouncedSearchTerm,
@@ -454,7 +442,7 @@ export const RestoreBackup = ({
onValueChange={handleSearchChange}
className="h-9"
/>
- {isLoading ? (
+ {isPending ? (
Loading backup files...
diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx
index 55a09b25f..9aa118548 100644
--- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx
@@ -89,11 +89,11 @@ export const ShowBackups = ({
const mutation = mutationMap[key as keyof typeof mutationMap];
- const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutation
+ const { mutateAsync: manualBackup, isPending: isManualBackup } = mutation
? mutation
: api.backup.manualBackupMongo.useMutation();
- const { mutateAsync: deleteBackup, isLoading: isRemoving } =
+ const { mutateAsync: deleteBackup, isPending: isRemoving } =
api.backup.remove.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
index bf0173956..59b939008 100644
--- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
+++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
@@ -402,7 +402,7 @@ export const DockerLogsId: React.FC = ({
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (
{
- const { data, isLoading } = api.docker.getContainers.useQuery({
+ const { data, isPending } = api.docker.getContainers.useQuery({
serverId,
});
@@ -137,7 +137,7 @@ export const ShowContainers = ({ serverId }: Props) => {
- {isLoading ? (
+ {isPending ? (
Loading...
@@ -192,7 +192,7 @@ export const ShowContainers = ({ serverId }: Props) => {
colSpan={columns.length}
className="h-24 text-center"
>
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx
index 8c848a0dc..288208fb1 100644
--- a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx
+++ b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -7,6 +7,7 @@ import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
@@ -16,6 +17,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
+import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
@@ -47,8 +49,9 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
},
);
const [canEdit, setCanEdit] = useState(true);
+ const [skipYamlValidation, setSkipYamlValidation] = useState(false);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.settings.updateTraefikFile.useMutation();
const form = useForm({
@@ -66,13 +69,15 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
- const { valid, error } = validateAndFormatYAML(data.traefikConfig);
- if (!valid) {
- form.setError("traefikConfig", {
- type: "manual",
- message: error || "Invalid YAML",
- });
- return;
+ if (!skipYamlValidation) {
+ const { valid, error } = validateAndFormatYAML(data.traefikConfig);
+ if (!valid) {
+ form.setError("traefikConfig", {
+ type: "manual",
+ message: error || "Invalid YAML",
+ });
+ return;
+ }
}
form.clearErrors("traefikConfig");
await mutateAsync({
@@ -153,14 +158,37 @@ routers:
/>
)}
-
-
- Update
-
+
+
+
+ setSkipYamlValidation(checked === true)
+ }
+ />
+
+ Skip YAML validation (for Go templating)
+
+
+
+ Traefik supports Go templating in dynamic configs (e.g.{" "}
+ {"{{range}}"}). Configs using
+ templates will fail standard YAML validation. Check this to save
+ without validation.
+
+
+
+ Update
+
+
diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx
index 8745db286..9917bc21b 100644
--- a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx
+++ b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,10 +48,10 @@ interface Props {
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
- const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm
({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -73,8 +73,8 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
toast.success("External Port updated");
await refetch();
})
- .catch(() => {
- toast.error("Error saving the external port");
+ .catch((error: Error) => {
+ toast.error(error?.message || "Error saving the external port");
});
};
@@ -140,7 +140,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
@@ -161,7 +161,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx
index 8e996846f..9d953279c 100644
--- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx
+++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx
@@ -28,13 +28,13 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
{ enabled: !!mariadbId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.mariadb.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.mariadb.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.mariadb.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx
index 62486e015..d181103b3 100644
--- a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx
+++ b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -41,8 +41,9 @@ interface Props {
}
export const UpdateMariadb = ({ mariadbId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.mariadb.update.useMutation();
const { data } = api.mariadb.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
utils.mariadb.one.invalidate({
mariadbId: mariadbId,
});
+ setIsOpen(false);
})
.catch(() => {
toast.error("Error updating the Mariadb");
@@ -87,7 +89,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
};
return (
-
+
{
/>
diff --git a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx
index d30061db5..ac79410b4 100644
--- a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx
+++ b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,10 +48,10 @@ interface Props {
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
- const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -73,8 +73,8 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
toast.success("External Port updated");
await refetch();
})
- .catch(() => {
- toast.error("Error saving the external port");
+ .catch((error: Error) => {
+ toast.error(error?.message || "Error saving the external port");
});
};
@@ -140,7 +140,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
@@ -160,7 +160,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx
index 23fbe51d3..47a29e6c1 100644
--- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx
+++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx
@@ -28,13 +28,13 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
{ enabled: !!mongoId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.mongo.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.mongo.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.mongo.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx
index e78abddbd..55bccce67 100644
--- a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx
+++ b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.mongo.update.useMutation();
const { data } = api.mongo.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
/>
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx
index 246ae296d..fc57221bd 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-compose-monitoring.tsx
@@ -34,7 +34,7 @@ export const ComposeFreeMonitoring = ({
appType = "stack",
serverId,
}: Props) => {
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
@@ -51,7 +51,7 @@ export const ComposeFreeMonitoring = ({
const [containerId, setContainerId] = useState();
- const { mutateAsync: restart, isLoading: isRestarting } =
+ const { mutateAsync: restart, isPending: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
@@ -81,7 +81,7 @@ export const ComposeFreeMonitoring = ({
value={containerAppName}
>
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx
index 1dd41e722..42bb361bb 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx
@@ -183,12 +183,13 @@ export const ContainerFreeMonitoring = ({
setCurrentData(data);
+ const MAX_DATA_POINTS = 300;
setAcummulativeData((prevData) => ({
- cpu: [...prevData.cpu, data.cpu],
- memory: [...prevData.memory, data.memory],
- block: [...prevData.block, data.block],
- network: [...prevData.network, data.network],
- disk: [...prevData.disk, data.disk],
+ cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
+ memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
+ block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
+ network: [...prevData.network, data.network].slice(-MAX_DATA_POINTS),
+ disk: [...prevData.disk, data.disk].slice(-MAX_DATA_POINTS),
}));
};
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx
index 026043806..1f584beea 100644
--- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring.tsx
@@ -39,7 +39,7 @@ export const ComposePaidMonitoring = ({
baseUrl,
token,
}: Props) => {
- const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
+ const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
@@ -56,7 +56,7 @@ export const ComposePaidMonitoring = ({
const [containerId, setContainerId] = useState
();
- const { mutateAsync: restart, isLoading: isRestarting } =
+ const { mutateAsync: restart, isPending: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
@@ -87,7 +87,7 @@ export const ComposePaidMonitoring = ({
value={containerAppName}
>
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx
index dfaa36f6b..b9ddad916 100644
--- a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx
+++ b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,10 +48,10 @@ interface Props {
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
- const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm
({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -73,8 +73,8 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
toast.success("External Port updated");
await refetch();
})
- .catch(() => {
- toast.error("Error saving the external port");
+ .catch((error: Error) => {
+ toast.error(error?.message || "Error saving the external port");
});
};
@@ -140,7 +140,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
@@ -160,7 +160,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx
index 045a717b7..1a55c1d1a 100644
--- a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx
+++ b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx
@@ -28,12 +28,12 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
{ enabled: !!mysqlId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.mysql.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.mysql.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.mysql.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx
index 353523aa0..3442d44e3 100644
--- a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx
+++ b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -41,8 +41,9 @@ interface Props {
}
export const UpdateMysql = ({ mysqlId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.mysql.update.useMutation();
const { data } = api.mysql.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
utils.mysql.one.invalidate({
mysqlId: mysqlId,
});
+ setIsOpen(false);
})
.catch(() => {
toast.error("Error updating MySQL");
@@ -87,7 +89,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
};
return (
-
+
{
/>
diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx
index c676e0233..c191bead5 100644
--- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx
+++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -52,7 +52,7 @@ export function AddOrganization({ organizationId }: Props) {
enabled: !!organizationId,
},
);
- const { mutateAsync, isLoading } = organizationId
+ const { mutateAsync, isPending } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const { refetch: refetchActiveOrganization } =
@@ -177,7 +177,7 @@ export function AddOrganization({ organizationId }: Props) {
)}
/>
-
+
{organizationId ? "Update organization" : "Create organization"}
diff --git a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx
index d9841716e..0921984ac 100644
--- a/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx
+++ b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx
index 46b3772a0..c38240a3f 100644
--- a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx
+++ b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,12 +48,12 @@ interface Props {
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
- const { mutateAsync, isLoading } =
+ const { mutateAsync, isPending } =
api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState("");
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -75,8 +75,8 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
toast.success("External Port updated");
await refetch();
})
- .catch(() => {
- toast.error("Error saving the external port");
+ .catch((error: Error) => {
+ toast.error(error?.message || "Error saving the external port");
});
};
@@ -142,7 +142,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
@@ -162,7 +162,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx
index de520053d..0e6b87e9e 100644
--- a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx
+++ b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx
@@ -28,13 +28,13 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
{ enabled: !!postgresId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.postgres.reload.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.postgres.stop.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.postgres.start.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx
index d4485862e..c83604b54 100644
--- a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx
+++ b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBox } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdatePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.postgres.update.useMutation();
const { data } = api.postgres.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
/>
{
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.application.create.useMutation();
const form = useForm({
@@ -283,7 +283,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
-
+
Create
diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx
index bb911373f..815c58ca8 100644
--- a/apps/dokploy/components/dashboard/project/add-compose.tsx
+++ b/apps/dokploy/components/dashboard/project/add-compose.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -75,7 +75,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.compose.create.useMutation();
// Get environment data to extract projectId
@@ -307,7 +307,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
-
+
Create
diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx
index 67d00b0d7..e14653880 100644
--- a/apps/dokploy/components/dashboard/project/add-database.tsx
+++ b/apps/dokploy/components/dashboard/project/add-database.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, Database, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -52,7 +52,7 @@ import {
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
-type DbType = typeof mySchema._type.type;
+type DbType = z.infer["type"];
const dockerImageDefaultPlaceholder: Record = {
mongo: "mongo:7",
@@ -196,7 +196,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
const shouldShowServerDropdown = hasServers;
- const form = useForm({
+ const form = useForm({
defaultValues: {
type: "postgres",
dockerImage: "",
diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx
index 72c42da49..ef9a88e6f 100644
--- a/apps/dokploy/components/dashboard/project/add-template.tsx
+++ b/apps/dokploy/components/dashboard/project/add-template.tsx
@@ -116,7 +116,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
- const { data: tags, isLoading: isLoadingTags } = api.compose.getTags.useQuery(
+ const { data: tags, isPending: isLoadingTags } = api.compose.getTags.useQuery(
{ baseUrl: customBaseUrl },
{
enabled: open,
@@ -125,7 +125,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
const utils = api.useUtils();
const [serverId, setServerId] = useState(undefined);
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.compose.deployTemplate.useMutation();
const templates =
@@ -512,7 +512,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
Cancel
{
const promise = mutateAsync({
serverId:
diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx
index 678928990..3e28a248b 100644
--- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx
+++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx
@@ -93,7 +93,7 @@ export const AdvancedEnvironmentSelector = ({
await createEnvironment.mutateAsync({
projectId,
name: name.trim(),
- description: description.trim() || null,
+ description: description.trim() || undefined,
});
toast.success("Environment created successfully");
@@ -115,7 +115,7 @@ export const AdvancedEnvironmentSelector = ({
await updateEnvironment.mutateAsync({
environmentId: selectedEnvironment.environmentId,
name: name.trim(),
- description: description.trim() || null,
+ description: description.trim() || undefined,
});
toast.success("Environment updated successfully");
@@ -168,7 +168,7 @@ export const AdvancedEnvironmentSelector = ({
const result = await duplicateEnvironment.mutateAsync({
environmentId: environment.environmentId,
name: `${environment.name}-copy`,
- description: environment.description,
+ description: environment.description || undefined,
});
toast.success("Environment duplicated successfully");
@@ -334,9 +334,9 @@ export const AdvancedEnvironmentSelector = ({
- {createEnvironment.isLoading ? "Creating..." : "Create"}
+ {createEnvironment.isPending ? "Creating..." : "Create"}
@@ -387,9 +387,9 @@ export const AdvancedEnvironmentSelector = ({
- {updateEnvironment.isLoading ? "Updating..." : "Update"}
+ {updateEnvironment.isPending ? "Updating..." : "Update"}
@@ -427,12 +427,12 @@ export const AdvancedEnvironmentSelector = ({
variant="destructive"
onClick={handleDeleteEnvironment}
disabled={
- deleteEnvironment.isLoading ||
+ deleteEnvironment.isPending ||
haveServices ||
!selectedEnvironment
}
>
- {deleteEnvironment.isLoading ? "Deleting..." : "Delete"}
+ {deleteEnvironment.isPending ? "Deleting..." : "Delete"}
diff --git a/apps/dokploy/components/dashboard/project/ai/step-two.tsx b/apps/dokploy/components/dashboard/project/ai/step-two.tsx
index 09484bc57..e13ff40ad 100644
--- a/apps/dokploy/components/dashboard/project/ai/step-two.tsx
+++ b/apps/dokploy/components/dashboard/project/ai/step-two.tsx
@@ -28,7 +28,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
const suggestions = templateInfo.suggestions || [];
const selectedVariant = templateInfo.details;
- const { mutateAsync, isLoading, error, isError } =
+ const { mutateAsync, isPending, error, isError } =
api.ai.suggest.useMutation();
useEffect(() => {
@@ -184,7 +184,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
);
}
- if (isLoading) {
+ if (isPending) {
return (
diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx
index 3455f34cf..f84cf35dd 100644
--- a/apps/dokploy/components/dashboard/project/duplicate-project.tsx
+++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx
@@ -76,7 +76,7 @@ export const DuplicateProject = ({
selectedServiceIds.includes(service.id),
);
- const { mutateAsync: duplicateProject, isLoading } =
+ const { mutateAsync: duplicateProject, isPending } =
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
@@ -321,20 +321,20 @@ export const DuplicateProject = ({
setOpen(false)}
- disabled={isLoading}
+ disabled={isPending}
>
Cancel
- {isLoading ? (
+ {isPending ? (
<>
{duplicateType === "new-project"
diff --git a/apps/dokploy/components/dashboard/project/environment-variables.tsx b/apps/dokploy/components/dashboard/project/environment-variables.tsx
index e833fa779..13ca99448 100644
--- a/apps/dokploy/components/dashboard/project/environment-variables.tsx
+++ b/apps/dokploy/components/dashboard/project/environment-variables.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Terminal } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -41,7 +41,7 @@ interface Props {
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.environment.update.useMutation();
const { data } = api.environment.one.useQuery(
{
@@ -85,7 +85,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending && isOpen) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -95,7 +95,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading, isOpen]);
+ }, [form, onSubmit, isPending, isOpen]);
return (
@@ -158,7 +158,7 @@ API_KEY=your-api-key-here
)}
/>
-
+
Update
diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx
index 09fd36f84..d3305e864 100644
--- a/apps/dokploy/components/dashboard/projects/handle-project.tsx
+++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx
@@ -1,4 +1,5 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+
import { PlusIcon, SquarePen } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
@@ -80,7 +81,7 @@ export const HandleProject = ({ projectId }: Props) => {
description: "",
name: "",
},
- resolver: zodResolver(AddProjectSchema),
+ resolver: standardSchemaResolver(AddProjectSchema),
});
useEffect(() => {
diff --git a/apps/dokploy/components/dashboard/projects/project-environment.tsx b/apps/dokploy/components/dashboard/projects/project-environment.tsx
index cb6245f08..b02f9024a 100644
--- a/apps/dokploy/components/dashboard/projects/project-environment.tsx
+++ b/apps/dokploy/components/dashboard/projects/project-environment.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { FileIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -41,7 +41,7 @@ interface Props {
export const ProjectEnvironment = ({ projectId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.project.update.useMutation();
const { data } = api.project.one.useQuery(
{
@@ -84,7 +84,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
+ if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending && isOpen) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -94,7 +94,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading, isOpen]);
+ }, [form, onSubmit, isPending, isOpen]);
return (
@@ -155,7 +155,7 @@ PORT=3000
)}
/>
-
+
Update
diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx
index 8234593e1..c3d4d498b 100644
--- a/apps/dokploy/components/dashboard/projects/show.tsx
+++ b/apps/dokploy/components/dashboard/projects/show.tsx
@@ -63,7 +63,7 @@ export const ShowProjects = () => {
const utils = api.useUtils();
const router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { data, isLoading } = api.project.all.useQuery();
+ const { data, isPending } = api.project.all.useQuery();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
@@ -200,7 +200,7 @@ export const ShowProjects = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
@@ -430,7 +430,7 @@ export const ShowProjects = () => {
) : null}
-
+
@@ -439,7 +439,7 @@ export const ShowProjects = () => {
-
+
{project.description}
diff --git a/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx b/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx
index 8edd92389..ebc01200a 100644
--- a/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx
+++ b/apps/dokploy/components/dashboard/redis/general/show-external-redis-credentials.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -48,11 +48,11 @@ interface Props {
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.redis.one.useQuery({ redisId });
- const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
+ const { mutateAsync, isPending } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
- const form = useForm({
+ const form = useForm({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
@@ -74,8 +74,8 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
toast.success("External Port updated");
await refetch();
})
- .catch(() => {
- toast.error("Error saving the external port");
+ .catch((error: Error) => {
+ toast.error(error?.message || "Error saving the external port");
});
};
@@ -134,7 +134,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
@@ -154,7 +154,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
)}
-
+
Save
diff --git a/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx b/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx
index de70cc558..4300f9af3 100644
--- a/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx
+++ b/apps/dokploy/components/dashboard/redis/general/show-general-redis.tsx
@@ -28,12 +28,12 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
{ enabled: !!redisId },
);
- const { mutateAsync: reload, isLoading: isReloading } =
+ const { mutateAsync: reload, isPending: isReloading } =
api.redis.reload.useMutation();
- const { mutateAsync: start, isLoading: isStarting } =
+ const { mutateAsync: start, isPending: isStarting } =
api.redis.start.useMutation();
- const { mutateAsync: stop, isLoading: isStopping } =
+ const { mutateAsync: stop, isPending: isStopping } =
api.redis.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
diff --git a/apps/dokploy/components/dashboard/redis/update-redis.tsx b/apps/dokploy/components/dashboard/redis/update-redis.tsx
index 7d17552fa..b20e6b0c4 100644
--- a/apps/dokploy/components/dashboard/redis/update-redis.tsx
+++ b/apps/dokploy/components/dashboard/redis/update-redis.tsx
@@ -1,6 +1,6 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -41,8 +41,9 @@ interface Props {
}
export const UpdateRedis = ({ redisId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
- const { mutateAsync, error, isError, isLoading } =
+ const { mutateAsync, error, isError, isPending } =
api.redis.update.useMutation();
const { data } = api.redis.one.useQuery(
{
@@ -79,6 +80,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
utils.redis.one.invalidate({
redisId: redisId,
});
+ setIsOpen(false);
})
.catch(() => {
toast.error("Error updating Redis");
@@ -87,7 +89,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
};
return (
-
+
{
/>
diff --git a/apps/dokploy/components/dashboard/settings/ai-form.tsx b/apps/dokploy/components/dashboard/settings/ai-form.tsx
index f7f81c9cf..c3518fdcc 100644
--- a/apps/dokploy/components/dashboard/settings/ai-form.tsx
+++ b/apps/dokploy/components/dashboard/settings/ai-form.tsx
@@ -15,8 +15,8 @@ import { api } from "@/utils/api";
import { HandleAi } from "./handle-ai";
export const AiForm = () => {
- const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();
- const { mutateAsync, isLoading: isRemoving } = api.ai.delete.useMutation();
+ const { data: aiConfigs, refetch, isPending } = api.ai.getAll.useQuery();
+ const { mutateAsync, isPending: isRemoving } = api.ai.delete.useMutation();
return (
@@ -33,7 +33,7 @@ export const AiForm = () => {
{aiConfigs && aiConfigs?.length > 0 &&
}
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
index 15c7ed6e0..c6db49b5d 100644
--- a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
+++ b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { useState } from "react";
import { useForm } from "react-hook-form";
diff --git a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx
index efa68929f..2e90a9bb6 100644
--- a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx
+++ b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx
@@ -17,7 +17,7 @@ import { AddApiKey } from "./add-api-key";
export const ShowApiKeys = () => {
const { data, refetch } = api.user.get.useQuery();
- const { mutateAsync: deleteApiKey, isLoading: isLoadingDelete } =
+ const { mutateAsync: deleteApiKey, isPending: isLoadingDelete } =
api.user.deleteApiKey.useMutation();
return (
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
index 1460244c1..6e56bdd70 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx
@@ -11,7 +11,9 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -31,6 +33,7 @@ const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);
+/** Precio legacy / Hobby: $4.50/mo primer servidor, $3.50 siguientes; anual $45.90 primero, $35.70 siguientes. */
export const calculatePrice = (count: number, isAnnual = false) => {
if (isAnnual) {
if (count <= 1) return 45.9;
@@ -40,6 +43,27 @@ export const calculatePrice = (count: number, isAnnual = false) => {
return count * 3.5;
};
+/** Hobby: $4.50/mo per server; annual 20% off = $43.20/yr per server (4.5 * 12 * 0.8). */
+export const calculatePriceHobby = (count: number, isAnnual = false) => {
+ const perServerMonthly = 4.5;
+ const perServerAnnual = 43.2; // 4.5 * 12 * 0.8
+ return isAnnual ? count * perServerAnnual : count * perServerMonthly;
+};
+
+/** Startup: 3 servers included ($15/mo); extra servers $4.50/mo each. Annual 20% off. */
+export const STARTUP_SERVERS_INCLUDED = 3;
+export const calculatePriceStartup = (count: number, isAnnual = false) => {
+ const baseMonthly = 15;
+ const extraMonthly = 4.5;
+ const baseAnnual = 144; // 15 * 12 * 0.8
+ const extraAnnual = 43.2; // 4.5 * 12 * 0.8, consistent with Hobby annual
+ if (count <= STARTUP_SERVERS_INCLUDED)
+ return isAnnual ? baseAnnual : baseMonthly;
+ return isAnnual
+ ? baseAnnual + (count - STARTUP_SERVERS_INCLUDED) * extraAnnual
+ : baseMonthly + (count - STARTUP_SERVERS_INCLUDED) * extraMonthly;
+};
+
const navigationItems = [
{
name: "Subscription",
@@ -57,22 +81,41 @@ export const ShowBilling = () => {
const router = useRouter();
const { data: servers } = api.server.count.useQuery();
const { data: admin } = api.user.get.useQuery();
- const { data, isLoading } = api.stripe.getProducts.useQuery();
+ const { data, isPending } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
const { mutateAsync: createCustomerPortalSession } =
api.stripe.createCustomerPortalSession.useMutation();
+ const { mutateAsync: upgradeSubscription, isPending: isUpgrading } =
+ api.stripe.upgradeSubscription.useMutation();
+ const utils = api.useUtils();
const [serverQuantity, setServerQuantity] = useState(3);
const [isAnnual, setIsAnnual] = useState(false);
+ const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
+ null,
+ );
+ const [upgradeServerQty, setUpgradeServerQty] = useState(3);
+ /** Billing interval in the upgrade/update form; synced to current when data loads. */
+ const [updateFormAnnual, setUpdateFormAnnual] = useState(false);
- const handleCheckout = async (productId: string) => {
+ useEffect(() => {
+ if (data?.isAnnualCurrent !== undefined) {
+ setUpdateFormAnnual(data.isAnnualCurrent);
+ }
+ }, [data?.isAnnualCurrent]);
+
+ const handleCheckout = async (
+ tier: "legacy" | "hobby" | "startup",
+ productId: string,
+ ) => {
const stripe = await stripePromise;
if (data && data.subscriptions.length === 0) {
createCheckoutSession({
+ tier,
productId,
- serverQuantity: serverQuantity,
+ serverQuantity,
isAnnual,
}).then(async (session) => {
await stripe?.redirectToCheckout({
@@ -81,6 +124,8 @@ export const ShowBilling = () => {
});
}
};
+
+ const useNewPricing = data?.hobbyProductId && data?.startupProductId;
const products = data?.products.filter((product) => {
// @ts-ignore
const interval = product?.default_price?.recurring?.interval;
@@ -93,7 +138,7 @@ export const ShowBilling = () => {
return (
-
+
@@ -128,17 +173,6 @@ export const ShowBilling = () => {
-
setIsAnnual(e === "annual")}
- >
-
- Monthly
- Annual
-
-
{admin?.user.stripeSubscriptionId && (
Servers Plan
@@ -160,6 +194,429 @@ export const ShowBilling = () => {
)}
)}
+ {/* Upgrade: solo para usuarios en plan legacy con nuevos planes disponibles */}
+ {useNewPricing &&
+ data?.currentPlan === "legacy" &&
+ data?.subscriptions?.length > 0 && (
+
+
Upgrade your plan
+
+ You’re on the legacy plan. Switch to Hobby or Startup
+ (same benefits). You can also choose annual billing (20%
+ off). Stripe will prorate the change.
+
+
+
+ Billing interval
+
+
+ setUpdateFormAnnual(false)}
+ >
+ Monthly
+
+ setUpdateFormAnnual(true)}
+ >
+ Annual (20% off)
+
+
+
+
New plan
+
+ setUpgradeTier("hobby")}
+ >
+ Hobby
+
+ setUpgradeTier("startup")}
+ >
+ Startup
+
+
+
+ {upgradeTier && (
+
+
+ Servers
+ {upgradeTier === "startup" &&
+ ` (min. ${STARTUP_SERVERS_INCLUDED})`}
+
+
+
+ setUpgradeServerQty((q) =>
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ q - 1,
+ ),
+ )
+ }
+ >
+
+
+
{
+ const v =
+ Number((e.target as HTMLInputElement).value) ||
+ 0;
+ setUpgradeServerQty(
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ v,
+ ),
+ );
+ }}
+ className="w-20 h-8"
+ />
+ setUpgradeServerQty((q) => q + 1)}
+ >
+
+
+
+
+ {upgradeTier === "hobby"
+ ? `$${calculatePriceHobby(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`
+ : `$${calculatePriceStartup(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`}
+
+
+
+ Current plan: Legacy
+
+
+ New plan:{" "}
+ {upgradeTier === "startup"
+ ? "Startup"
+ : "Hobby"}{" "}
+ · {upgradeServerQty} server
+ {upgradeServerQty !== 1 ? "s" : ""} · $
+ {upgradeTier === "hobby"
+ ? calculatePriceHobby(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)
+ : calculatePriceStartup(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)}
+ /{updateFormAnnual ? "yr" : "mo"} (
+ {updateFormAnnual ? "annual" : "monthly"})
+
+
+ Stripe will prorate the change.
+
+
+ }
+ type="default"
+ onClick={async () => {
+ if (!upgradeTier) return;
+ try {
+ await upgradeSubscription({
+ tier: upgradeTier,
+ serverQuantity: upgradeServerQty,
+ isAnnual: updateFormAnnual,
+ });
+ await utils.stripe.getProducts.invalidate();
+ await utils.user.get.invalidate();
+ setUpgradeTier(null);
+ toast.success("Plan upgraded successfully");
+ } catch {
+ toast.error("Error upgrading plan");
+ }
+ }}
+ >
+
+ {isUpgrading ? (
+ <>
+
+ Upgrading…
+ >
+ ) : (
+ "Upgrade plan"
+ )}
+
+
+
+ )}
+
+ )}
+ {/* Cambiar plan o cantidad de servidores (usuarios en Hobby o Startup; el portal no permite esto) */}
+ {useNewPricing &&
+ (data?.currentPlan === "hobby" ||
+ data?.currentPlan === "startup") &&
+ data?.subscriptions?.length > 0 && (
+
+
+ Change plan or number of servers
+
+
+ Your current plan:{" "}
+
+ {data?.currentPlan === "startup" ? "Startup" : "Hobby"}
+
+ {" · "}
+
+ {admin?.user.serversQuantity ?? 0} server
+ {(admin?.user.serversQuantity ?? 0) !== 1 ? "s" : ""}
+
+ {data?.currentPriceAmount != null && (
+ <>
+ {" · "}
+
+ ${data.currentPriceAmount.toFixed(2)}/
+ {data?.isAnnualCurrent ? "yr" : "mo"}
+
+ >
+ )}{" "}
+ ({data?.isAnnualCurrent ? "annual" : "monthly"} billing).
+
+
+ Add more servers, switch between Hobby and Startup, or
+ change to annual billing (20% off). Stripe will prorate
+ the change.
+
+
+
+ Billing interval
+
+
+ setUpdateFormAnnual(false)}
+ >
+ Monthly
+
+ setUpdateFormAnnual(true)}
+ >
+ Annual (20% off)
+
+
+
+
Plan
+
+ setUpgradeTier("hobby")}
+ >
+ Hobby
+
+ setUpgradeTier("startup")}
+ >
+ Startup
+
+
+
+ {upgradeTier && (
+
+
+ Servers
+ {upgradeTier === "startup" &&
+ ` (min. ${STARTUP_SERVERS_INCLUDED})`}
+
+
+
+ setUpgradeServerQty((q) =>
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ q - 1,
+ ),
+ )
+ }
+ >
+
+
+
{
+ const v =
+ Number((e.target as HTMLInputElement).value) ||
+ 0;
+ setUpgradeServerQty(
+ Math.max(
+ upgradeTier === "startup"
+ ? STARTUP_SERVERS_INCLUDED
+ : 1,
+ v,
+ ),
+ );
+ }}
+ className="w-20 h-8"
+ />
+ setUpgradeServerQty((q) => q + 1)}
+ >
+
+
+
+
+ {upgradeTier === "hobby"
+ ? `$${calculatePriceHobby(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`
+ : `$${calculatePriceStartup(upgradeServerQty, updateFormAnnual).toFixed(2)} per ${updateFormAnnual ? "year" : "month"}`}
+
+
+
+ Current plan:{" "}
+ {data?.currentPlan === "startup"
+ ? "Startup"
+ : "Hobby"}{" "}
+ · {admin?.user.serversQuantity ?? 0} server
+ {(admin?.user.serversQuantity ?? 0) !== 1
+ ? "s"
+ : ""}{" "}
+ ·{" "}
+ {data?.currentPriceAmount != null
+ ? `$${data.currentPriceAmount.toFixed(2)}/${data?.isAnnualCurrent ? "yr" : "mo"}`
+ : ""}{" "}
+ ({data?.isAnnualCurrent ? "annual" : "monthly"})
+
+
+ New plan:{" "}
+ {upgradeTier === "startup"
+ ? "Startup"
+ : "Hobby"}{" "}
+ · {upgradeServerQty} server
+ {upgradeServerQty !== 1 ? "s" : ""} · $
+ {upgradeTier === "hobby"
+ ? calculatePriceHobby(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)
+ : calculatePriceStartup(
+ upgradeServerQty,
+ updateFormAnnual,
+ ).toFixed(2)}
+ /{updateFormAnnual ? "yr" : "mo"} (
+ {updateFormAnnual ? "annual" : "monthly"})
+
+
+ Stripe will prorate the change.
+
+
+ }
+ type="default"
+ onClick={async () => {
+ if (!upgradeTier) return;
+ try {
+ await upgradeSubscription({
+ tier: upgradeTier,
+ serverQuantity: upgradeServerQty,
+ isAnnual: updateFormAnnual,
+ });
+ await utils.stripe.getProducts.invalidate();
+
+ // add delay of 3 seconds
+ await new Promise((resolve) =>
+ setTimeout(resolve, 3000),
+ );
+ await utils.user.get.invalidate();
+ setUpgradeTier(null);
+ toast.success(
+ "Subscription updated successfully",
+ );
+ } catch {
+ toast.error("Error updating subscription");
+ }
+ }}
+ >
+
+ {isUpgrading ? (
+ <>
+
+ Updating…
+ >
+ ) : (
+ "Update subscription"
+ )}
+
+
+
+ )}
+
+ )}
Need Help? We are here to help you.
@@ -186,13 +643,350 @@ export const ShowBilling = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
+ ) : useNewPricing ? (
+ <>
+ setIsAnnual(e === "annual")}
+ >
+
+ Monthly
+ Annual (20% off)
+
+
+
+ {/* Hobby */}
+
+ {isAnnual && (
+
+ 20% off
+
+ )}
+
+ Hobby
+
+
+ Everything an individual developer needs
+
+
+
+ $
+ {calculatePriceHobby(
+ serverQuantity,
+ isAnnual,
+ ).toFixed(2)}
+ /{isAnnual ? "yr" : "mo"}
+
+
+ Add more servers as you'd like for{" "}
+ {isAnnual ? "$43.20/yr" : "$4.50/mo"}
+
+ {isAnnual && (
+
+ $
+ {(
+ calculatePriceHobby(serverQuantity, true) / 12
+ ).toFixed(2)}
+ /mo
+
+ )}
+
+
+ {[
+ "Unlimited Deployments",
+ "Unlimited Databases",
+ "Unlimited Applications",
+ "1 Server Included",
+ "1 Organization",
+ "1 User",
+ "2 Environments",
+ "1 Volume Backup per Application",
+ "1 Backup per Database",
+ "1 Scheduled Job per Application",
+ "Community Support (Discord)",
+ ].map((f) => (
+
+
+ {f}
+
+ ))}
+
+
+
+
+ Servers:
+
+
+ setServerQuantity((q) => Math.max(1, q - 1))
+ }
+ >
+
+
+
+ setServerQuantity(
+ Math.max(
+ 1,
+ Number(
+ (e.target as HTMLInputElement).value,
+ ) || 1,
+ ),
+ )
+ }
+ className="text-center"
+ />
+ setServerQuantity((q) => q + 1)}
+ >
+
+
+
+
+ {admin?.user.stripeCustomerId && (
+ {
+ const session =
+ await createCustomerPortalSession();
+ window.open(session.url);
+ }}
+ >
+ Manage Subscription
+
+ )}
+ {(data?.subscriptions?.length ?? 0) === 0 && (
+
+ handleCheckout("hobby", data!.hobbyProductId!)
+ }
+ disabled={serverQuantity < 1}
+ >
+ Get Started
+
+ )}
+
+
+
+
+ {/* Startup - Recommended */}
+
+
+
+ Recommended
+
+ {isAnnual && (
+
+ 20% off
+
+ )}
+
+
+ Startup
+
+
+ Perfect for small to mid-size teams
+
+
+
+ $
+ {calculatePriceStartup(
+ serverQuantity,
+ isAnnual,
+ ).toFixed(2)}
+ /{isAnnual ? "yr" : "mo"}
+
+
+ Add more servers as you'd like for{" "}
+ {isAnnual ? "$43.20/yr" : "$4.50/mo"}
+
+ {isAnnual && (
+
+ $
+ {(
+ calculatePriceStartup(serverQuantity, true) / 12
+ ).toFixed(2)}
+ /mo
+
+ )}
+
+
+
+
+ All the features of Hobby, plus…
+
+ {[
+ "3 Servers Included",
+ "3 Organizations",
+ "Unlimited Users",
+ "Unlimited Environments",
+ "Unlimited Volume Backups",
+ "Unlimited Database Backups",
+ "Unlimited Scheduled Jobs",
+ "Basic RBAC (Admin, Developer)",
+ "2FA",
+ "Email and Chat Support",
+ ].map((f) => (
+
+
+ {f}
+
+ ))}
+
+
+
+
+ Servers (min. {STARTUP_SERVERS_INCLUDED} included)
+
+
+
+ setServerQuantity((q) =>
+ Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
+ )
+ }
+ >
+
+
+
+ setServerQuantity(
+ Math.max(
+ STARTUP_SERVERS_INCLUDED,
+ Number(
+ (e.target as HTMLInputElement).value,
+ ) || STARTUP_SERVERS_INCLUDED,
+ ),
+ )
+ }
+ className="h-8 text-center"
+ />
+ setServerQuantity((q) => q + 1)}
+ >
+
+
+
+
+
+ {admin?.user.stripeCustomerId && (
+ {
+ const session =
+ await createCustomerPortalSession();
+ window.open(session.url);
+ }}
+ >
+ Manage Subscription
+
+ )}
+ {(data?.subscriptions?.length ?? 0) === 0 && (
+
+ handleCheckout(
+ "startup",
+ data!.startupProductId!,
+ )
+ }
+ disabled={
+ serverQuantity < STARTUP_SERVERS_INCLUDED
+ }
+ >
+ Get Started
+
+ )}
+
+
+
+
+ {/* Enterprise */}
+
+
+ Enterprise
+
+
+ For large organizations who want more control
+
+
+
+
+
+ All the features of Startup, plus…
+
+ {[
+ "Up to Unlimited Servers",
+ "Up to Unlimited Organizations",
+ "Fine-grained RBAC",
+ "Complete Hosting Flexibility",
+ "SSO / SAML (Azure, OKTA, etc)",
+ "Audit Logs",
+ "MSA/SLA",
+ "White Labeling",
+ "Priority Support and Services",
+ ].map((f) => (
+
+
+ {f}
+
+ ))}
+
+
+
+ Contact Sales
+
+
+
+
+ >
) : (
<>
+ setIsAnnual(e === "annual")}
+ >
+
+ Monthly
+ Annual (20% off)
+
+
{products?.map((product) => {
const featured = true;
return (
@@ -311,15 +1105,7 @@ export const ShowBilling = () => {
-
0
- ? "justify-between"
- : "justify-end",
- "flex flex-row items-center gap-2 mt-4",
- )}
- >
+
{admin?.user.stripeCustomerId && (
{
onClick={async () => {
const session =
await createCustomerPortalSession();
-
window.open(session.url);
}}
>
Manage Subscription
)}
-
- {data?.subscriptions?.length === 0 && (
-
- {
- handleCheckout(product.id);
- }}
- disabled={serverQuantity < 1}
- >
- Subscribe
-
-
+ {(data?.subscriptions?.length ?? 0) === 0 && (
+
{
+ handleCheckout("legacy", product.id);
+ }}
+ disabled={serverQuantity < 1}
+ >
+ Subscribe
+
)}
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx
index 73cc82efc..b10e09596 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-invoices.tsx
@@ -53,11 +53,11 @@ const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
};
export const ShowInvoices = () => {
- const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
+ const { data: invoices, isPending } = api.stripe.getInvoices.useQuery();
return (
- {isLoading ? (
+ {isPending ? (
Loading invoices...
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx b/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx
index f87ca58c7..ca1407d7e 100644
--- a/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx
+++ b/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx
@@ -12,7 +12,7 @@ export const ShowWelcomeDokploy = () => {
const { data } = api.user.get.useQuery();
const [open, setOpen] = useState(false);
- const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
+ const { data: isCloud, isPending } = api.settings.isCloud.useQuery();
if (!isCloud || data?.role !== "admin") {
return null;
@@ -20,14 +20,14 @@ export const ShowWelcomeDokploy = () => {
useEffect(() => {
if (
- !isLoading &&
+ !isPending &&
isCloud &&
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
data?.role === "owner"
) {
setOpen(true);
}
- }, [isCloud, isLoading]);
+ }, [isCloud, isPending]);
const handleClose = (isOpen: boolean) => {
if (data?.role === "owner") {
diff --git a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx
index 6f7ef6821..bc29a4c95 100644
--- a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx
+++ b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -62,7 +62,7 @@ export const AddCertificate = () => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { mutateAsync, isError, error, isLoading } =
+ const { mutateAsync, isError, error, isPending } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
@@ -247,7 +247,7 @@ export const AddCertificate = () => {
diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
index 8356d89c6..e861c9027 100644
--- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
+++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
@@ -15,9 +15,9 @@ import { AddCertificate } from "./add-certificate";
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
export const ShowCertificates = () => {
- const { mutateAsync, isLoading: isRemoving } =
+ const { mutateAsync, isPending: isRemoving } =
api.certificates.remove.useMutation();
- const { data, isLoading, refetch } = api.certificates.all.useQuery();
+ const { data, isPending, refetch } = api.certificates.all.useQuery();
return (
@@ -40,7 +40,7 @@ export const ShowCertificates = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx
index 36dda311c..a24e4dc2f 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx
@@ -15,7 +15,7 @@ interface Props {
}
export const AddManager = ({ serverId }: Props) => {
- const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
+ const { data, isPending, error, isError } = api.cluster.addManager.useQuery({
serverId,
});
@@ -27,7 +27,7 @@ export const AddManager = ({ serverId }: Props) => {
Add a new manager
{isError &&
{error?.message} }
- {isLoading ? (
+ {isPending ? (
) : (
<>
diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx
index b88fdd8e8..c7f580caa 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx
@@ -48,7 +48,7 @@ interface Props {
}
export const ShowNodes = ({ serverId }: Props) => {
- const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
+ const { data, isPending, refetch } = api.cluster.getNodes.useQuery({
serverId,
});
const { data: registry } = api.registry.all.useQuery();
@@ -75,7 +75,7 @@ export const ShowNodes = ({ serverId }: Props) => {
)}
- {isLoading ? (
+ {isPending ? (
diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx
index c73e458ef..40a673265 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx
@@ -15,7 +15,7 @@ interface Props {
}
export const AddWorker = ({ serverId }: Props) => {
- const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
+ const { data, isPending, error, isError } = api.cluster.addWorker.useQuery({
serverId,
});
@@ -26,7 +26,7 @@ export const AddWorker = ({ serverId }: Props) => {
Add a new worker
{isError && {error?.message} }
- {isLoading ? (
+ {isPending ? (
) : (
<>
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx
index 979276995..e22285c73 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -105,13 +105,13 @@ export const HandleRegistry = ({ registryId }: Props) => {
const servers = [...(deployServers || []), ...(buildServers || [])];
const {
mutateAsync: testRegistry,
- isLoading,
+ isPending,
error: testRegistryError,
isError: testRegistryIsError,
} = api.registry.testRegistry.useMutation();
const {
mutateAsync: testRegistryById,
- isLoading: isLoadingById,
+ isPending: isPendingById,
error: testRegistryByIdError,
isError: testRegistryByIdIsError,
} = api.registry.testRegistryById.useMutation();
@@ -451,7 +451,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
{
// When editing with empty password, use the existing password from DB
if (registryId && (!password || password.length === 0)) {
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx
index c5dd60622..cbaf5de2f 100644
--- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx
+++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx
@@ -13,9 +13,9 @@ import { api } from "@/utils/api";
import { HandleRegistry } from "./handle-registry";
export const ShowRegistry = () => {
- const { mutateAsync, isLoading: isRemoving } =
+ const { mutateAsync, isPending: isRemoving } =
api.registry.remove.useMutation();
- const { data, isLoading, refetch } = api.registry.all.useQuery();
+ const { data, isPending, refetch } = api.registry.all.useQuery();
return (
@@ -31,7 +31,7 @@ export const ShowRegistry = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx
index dae069e91..966c8e5f5 100644
--- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx
+++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -60,7 +60,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { mutateAsync, isError, error, isLoading } = destinationId
+ const { mutateAsync, isError, error, isPending } = destinationId
? api.destination.update.useMutation()
: api.destination.create.useMutation();
@@ -75,7 +75,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
);
const {
mutateAsync: testConnection,
- isLoading: isLoadingConnection,
+ isPending: isPendingConnection,
error: connectionError,
isError: isErrorConnection,
} = api.destination.testConnection.useMutation();
@@ -410,7 +410,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
{
await handleTestConnection(form.getValues("serverId"));
}}
@@ -420,7 +420,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
) : (
{
@@ -432,7 +432,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
)}
diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx
index f0ad39807..3cb29d54a 100644
--- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx
+++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx
@@ -13,8 +13,8 @@ import { api } from "@/utils/api";
import { HandleDestinations } from "./handle-destinations";
export const ShowDestinations = () => {
- const { data, isLoading, refetch } = api.destination.all.useQuery();
- const { mutateAsync, isLoading: isRemoving } =
+ const { data, isPending, refetch } = api.destination.all.useQuery();
+ const { mutateAsync, isPending: isRemoving } =
api.destination.remove.useMutation();
return (
@@ -31,7 +31,7 @@ export const ShowDestinations = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx
index c933a0b8c..51a2291c1 100644
--- a/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/bitbucket/add-bitbucket-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
diff --git a/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx
index 3cccdff71..045b0196f 100644
--- a/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/bitbucket/edit-bitbucket-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -58,7 +58,7 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.bitbucket.update.useMutation();
- const { mutateAsync: testConnection, isLoading } =
+ const { mutateAsync: testConnection, isPending } =
api.bitbucket.testConnection.useMutation();
const form = useForm
({
defaultValues: {
@@ -257,7 +257,7 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
{
await testConnection({
bitbucketId,
diff --git a/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx
index 4cb6bd50e..89307b1ce 100644
--- a/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -19,6 +19,7 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
FormLabel,
@@ -39,6 +40,10 @@ const Schema = z.object({
giteaUrl: z.string().min(1, {
message: "Gitea URL is required",
}),
+ giteaInternalUrl: z
+ .union([z.string().url(), z.literal("")])
+ .optional()
+ .transform((v) => (v === "" ? undefined : v)),
clientId: z.string().min(1, {
message: "Client ID is required",
}),
@@ -63,13 +68,14 @@ export const AddGiteaProvider = () => {
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
- const form = useForm({
+ const form = useForm({
defaultValues: {
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
+ giteaInternalUrl: "",
},
resolver: zodResolver(Schema),
});
@@ -83,6 +89,7 @@ export const AddGiteaProvider = () => {
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
+ giteaInternalUrl: "",
});
}, [form, webhookUrl, isOpen]);
@@ -95,6 +102,7 @@ export const AddGiteaProvider = () => {
name: data.name,
redirectUri: data.redirectUri,
giteaUrl: data.giteaUrl,
+ giteaInternalUrl: data.giteaInternalUrl || undefined,
organizationName: data.organizationName,
})) as unknown as GiteaProviderResponse;
@@ -223,6 +231,29 @@ export const AddGiteaProvider = () => {
)}
/>
+ (
+
+ Internal URL (Optional)
+
+
+
+
+ Use when Gitea runs on the same instance as Dokploy.
+ Used for OAuth token exchange to reach Gitea via
+ internal network (e.g. Docker service name).
+
+
+
+ )}
+ />
+
(v === "" ? undefined : v)),
clientId: z.string().min(1, "Client ID is required"),
clientSecret: z.string().min(1, "Client Secret is required"),
});
@@ -46,8 +51,8 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
isLoading,
refetch,
} = api.gitea.one.useQuery({ giteaId });
- const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
- const { mutateAsync: testConnection, isLoading: isTesting } =
+ const { mutateAsync, isPending: isUpdating } = api.gitea.update.useMutation();
+ const { mutateAsync: testConnection, isPending: isTesting } =
api.gitea.testConnection.useMutation();
const url = useUrl();
const utils = api.useUtils();
@@ -89,11 +94,12 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
}
}, [router.query, router.isReady, refetch]);
- const form = useForm>({
+ const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
giteaUrl: "https://gitea.com",
+ giteaInternalUrl: "",
clientId: "",
clientSecret: "",
},
@@ -104,6 +110,7 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
form.reset({
name: gitea.gitProvider?.name || "",
giteaUrl: gitea.giteaUrl || "https://gitea.com",
+ giteaInternalUrl: gitea.giteaInternalUrl || "",
clientId: gitea.clientId || "",
clientSecret: gitea.clientSecret || "",
});
@@ -116,6 +123,7 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
name: values.name,
giteaUrl: values.giteaUrl,
+ giteaInternalUrl: values.giteaInternalUrl ?? null,
clientId: values.clientId,
clientSecret: values.clientSecret,
})
@@ -224,6 +232,28 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
)}
/>
+ (
+
+ Internal URL (Optional)
+
+
+
+
+ Use when Gitea runs on the same instance as Dokploy. Used
+ for OAuth token exchange to reach Gitea via internal network
+ (e.g. Docker service name).
+
+
+
+ )}
+ />
{
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.github.update.useMutation();
- const { mutateAsync: testConnection, isLoading } =
+ const { mutateAsync: testConnection, isPending } =
api.github.testConnection.useMutation();
const form = useForm({
defaultValues: {
@@ -151,7 +151,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
{
await testConnection({
githubId,
diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx
index f1369ade9..7c637f5ef 100644
--- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx
@@ -1,4 +1,4 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -19,6 +19,7 @@ import {
import {
Form,
FormControl,
+ FormDescription,
FormField,
FormItem,
FormLabel,
@@ -35,6 +36,10 @@ const Schema = z.object({
gitlabUrl: z.string().min(1, {
message: "GitLab URL is required",
}),
+ gitlabInternalUrl: z
+ .union([z.string().url(), z.literal("")])
+ .optional()
+ .transform((v) => (v === "" ? undefined : v)),
applicationId: z.string().min(1, {
message: "Application ID is required",
}),
@@ -58,7 +63,7 @@ export const AddGitlabProvider = () => {
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
const webhookUrl = `${url}/api/providers/gitlab/callback`;
- const form = useForm({
+ const form = useForm({
defaultValues: {
applicationId: "",
applicationSecret: "",
@@ -66,6 +71,7 @@ export const AddGitlabProvider = () => {
redirectUri: webhookUrl,
name: "",
gitlabUrl: "https://gitlab.com",
+ gitlabInternalUrl: "",
},
resolver: zodResolver(Schema),
});
@@ -80,6 +86,7 @@ export const AddGitlabProvider = () => {
redirectUri: webhookUrl,
name: "",
gitlabUrl: "https://gitlab.com",
+ gitlabInternalUrl: "",
});
}, [form, isOpen]);
@@ -92,6 +99,7 @@ export const AddGitlabProvider = () => {
name: data.name || "",
redirectUri: data.redirectUri || "",
gitlabUrl: data.gitlabUrl || "https://gitlab.com",
+ gitlabInternalUrl: data.gitlabInternalUrl || undefined,
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -192,6 +200,29 @@ export const AddGitlabProvider = () => {
)}
/>
+ (
+
+ Internal URL (Optional)
+
+
+
+
+ Use when GitLab runs on the same instance as Dokploy.
+ Used for OAuth token exchange to reach GitLab via
+ internal network (e.g. Docker service name).
+
+
+
+ )}
+ />
+
(v === "" ? undefined : v)),
groupName: z.string().optional(),
});
@@ -54,13 +59,14 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.gitlab.update.useMutation();
- const { mutateAsync: testConnection, isLoading } =
+ const { mutateAsync: testConnection, isPending } =
api.gitlab.testConnection.useMutation();
- const form = useForm({
+ const form = useForm({
defaultValues: {
groupName: "",
name: "",
gitlabUrl: "https://gitlab.com",
+ gitlabInternalUrl: "",
},
resolver: zodResolver(Schema),
});
@@ -72,6 +78,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
groupName: gitlab?.groupName || "",
name: gitlab?.gitProvider.name || "",
gitlabUrl: gitlab?.gitlabUrl || "",
+ gitlabInternalUrl: gitlab?.gitlabInternalUrl || "",
});
}, [form, isOpen]);
@@ -82,6 +89,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
groupName: data.groupName || "",
name: data.name || "",
gitlabUrl: data.gitlabUrl || "",
+ gitlabInternalUrl: data.gitlabInternalUrl ?? null,
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -151,6 +159,29 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
)}
/>
+ (
+
+ Internal URL (Optional)
+
+
+
+
+ Use when GitLab runs on the same instance as Dokploy.
+ Used for OAuth token exchange to reach GitLab via
+ internal network (e.g. Docker service name).
+
+
+
+ )}
+ />
+
{
{
await testConnection({
gitlabId,
diff --git a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx
index 5e4c12696..a96bcf26c 100644
--- a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx
+++ b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx
@@ -36,8 +36,8 @@ import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
export const ShowGitProviders = () => {
- const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
- const { mutateAsync, isLoading: isRemoving } =
+ const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
+ const { mutateAsync, isPending: isRemoving } =
api.gitProvider.remove.useMutation();
const url = useUrl();
@@ -66,7 +66,7 @@ export const ShowGitProviders = () => {
- {isLoading ? (
+ {isPending ? (
Loading...
diff --git a/apps/dokploy/components/dashboard/settings/handle-ai.tsx b/apps/dokploy/components/dashboard/settings/handle-ai.tsx
index b5a0d51d8..d600d3a8e 100644
--- a/apps/dokploy/components/dashboard/settings/handle-ai.tsx
+++ b/apps/dokploy/components/dashboard/settings/handle-ai.tsx
@@ -1,5 +1,5 @@
"use client";
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Check, ChevronDown, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -57,7 +57,6 @@ interface Props {
export const HandleAi = ({ aiId }: Props) => {
const utils = api.useUtils();
- const [error, setError] = useState
(null);
const [open, setOpen] = useState(false);
const [modelPopoverOpen, setModelPopoverOpen] = useState(false);
const [modelSearch, setModelSearch] = useState("");
@@ -69,7 +68,7 @@ export const HandleAi = ({ aiId }: Props) => {
enabled: !!aiId,
},
);
- const { mutateAsync, isLoading } = aiId
+ const { mutateAsync, isPending } = aiId
? api.ai.update.useMutation()
: api.ai.create.useMutation();
@@ -102,19 +101,19 @@ export const HandleAi = ({ aiId }: Props) => {
const apiKey = form.watch("apiKey");
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
- const { data: models, isLoading: isLoadingServerModels } =
- api.ai.getModels.useQuery(
- {
- apiUrl: apiUrl ?? "",
- apiKey: apiKey ?? "",
- },
- {
- enabled: !!apiUrl && (isOllama || !!apiKey),
- onError: (error) => {
- setError(`Failed to fetch models: ${error.message}`);
- },
- },
- );
+ const {
+ data: models,
+ isPending: isLoadingServerModels,
+ error: modelsError,
+ } = api.ai.getModels.useQuery(
+ {
+ apiUrl: apiUrl ?? "",
+ apiKey: apiKey ?? "",
+ },
+ {
+ enabled: !!apiUrl && (isOllama || !!apiKey),
+ },
+ );
const onSubmit = async (data: Schema) => {
try {
@@ -169,7 +168,9 @@ export const HandleAi = ({ aiId }: Props) => {