+
{deployment.pid && deployment.status === "running" && (
Kill Process
@@ -364,10 +398,38 @@ export const ShowDeployments = ({
onClick={() => {
setActiveLog(deployment);
}}
+ className="w-full sm:w-auto"
>
View
+ {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" && (
@@ -405,6 +467,7 @@ export const ShowDeployments = ({
variant="secondary"
size="sm"
isLoading={isRollingBack}
+ className="w-full sm:w-auto"
>
Rollback
diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx
index a37f4959b..9b2aa4bd3 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";
@@ -169,11 +169,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 } =
@@ -257,7 +257,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(() => {
@@ -791,7 +791,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..06428ae21 100644
--- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
@@ -50,6 +50,9 @@ interface Props {
}
export const ShowDomains = ({ id, type }: Props) => {
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canCreateDomain = permissions?.domain.create ?? false;
+ const canDeleteDomain = permissions?.domain.delete ?? false;
const { data: application } =
type === "application"
? api.application.one.useQuery(
@@ -97,7 +100,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) => {
@@ -149,7 +152,7 @@ export const ShowDomains = ({ id, type }: Props) => {
- {data && data?.length > 0 && (
+ {canCreateDomain && data && data?.length > 0 && (
Add Domain
@@ -173,13 +176,15 @@ export const ShowDomains = ({ id, type }: Props) => {
To access the application it is required to set at least 1
domain
-
+ {canCreateDomain && (
+
+ )}
) : (
@@ -214,47 +219,51 @@ export const ShowDomains = ({ id, type }: Props) => {
}
/>
)}
-
-
-
-
-
-
{
- await deleteDomain({
- domainId: item.domainId,
- })
- .then((_data) => {
- refetch();
- toast.success(
- "Domain deleted successfully",
- );
+
+
+
+
+ )}
+ {canDeleteDomain && (
+ {
+ await deleteDomain({
+ domainId: item.domainId,
})
- .catch(() => {
- toast.error("Error deleting domain");
- });
- }}
- >
- {
+ refetch();
+ toast.success(
+ "Domain deleted successfully",
+ );
+ })
+ .catch(() => {
+ toast.error("Error deleting domain");
+ });
+ }}
>
-
-
-
+
+
+
+
+ )}
diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx
index 797a317a8..86f7a0dff 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";
@@ -36,16 +36,19 @@ interface Props {
}
export const ShowEnvironment = ({ id, type }: Props) => {
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canWrite = permissions?.envVars.write ?? false;
const queryMap = {
- postgres: () =>
- api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
- redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
- mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
+ compose: () =>
+ api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
+ libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
- compose: () =>
- api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
+ mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
+ postgres: () =>
+ api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
+ redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
@@ -53,14 +56,15 @@ export const ShowEnvironment = ({ id, type }: Props) => {
const [isEnvVisible, setIsEnvVisible] = useState(true);
const mutationMap = {
- postgres: () => api.postgres.update.useMutation(),
- redis: () => api.redis.update.useMutation(),
- mysql: () => api.mysql.update.useMutation(),
+ compose: () => api.compose.update.useMutation(),
+ libsql: () => api.libsql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
- compose: () => api.compose.update.useMutation(),
+ mysql: () => api.mysql.update.useMutation(),
+ postgres: () => api.postgres.update.useMutation(),
+ redis: () => api.redis.update.useMutation(),
};
- const { mutateAsync, isLoading } = mutationMap[type]
+ const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -85,12 +89,13 @@ export const ShowEnvironment = ({ id, type }: Props) => {
const onSubmit = async (formData: EnvironmentSchema) => {
mutateAsync({
+ composeId: id || "",
+ libsqlId: id || "",
+ mariadbId: id || "",
mongoId: id || "",
+ mysqlId: id || "",
postgresId: id || "",
redisId: id || "",
- mysqlId: id || "",
- mariadbId: id || "",
- composeId: id || "",
env: formData.environment,
})
.then(async () => {
@@ -111,7 +116,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 +126,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -185,25 +190,27 @@ PORT=3000
)}
/>
-
- {hasChanges && (
+ {canWrite && (
+
+ {hasChanges && (
+
+ Cancel
+
+ )}
- Cancel
+ Save
- )}
-
- Save
-
-
+
+ )}
diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx
index 5de03c367..fcfd81778 100644
--- a/apps/dokploy/components/dashboard/application/environment/show.tsx
+++ b/apps/dokploy/components/dashboard/application/environment/show.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";
@@ -31,7 +31,9 @@ interface Props {
}
export const ShowEnvironment = ({ applicationId }: Props) => {
- const { mutateAsync, isLoading } =
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canWrite = permissions?.envVars.write ?? false;
+ const { mutateAsync, isPending } =
api.application.saveEnvironment.useMutation();
const { data, refetch } = api.application.one.useQuery(
@@ -104,7 +106,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 +116,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
@@ -191,36 +193,40 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
Create Environment File
- When enabled, an .env file will be created during the
- build process. Disable this if you don't want to generate
- an environment file.
+ When enabled, an .env file will be created in the same
+ directory as your Dockerfile during the build process.
+ Disable this if you don't want to generate an environment
+ file.
)}
/>
)}
-
- {hasChanges && (
-
- Cancel
+ {canWrite && (
+
+ {hasChanges && (
+
+ Cancel
+
+ )}
+
+ Save
- )}
-
- Save
-
-
+
+ )}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx
index 1f54ddd58..a4fab46d9 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx
@@ -1,5 +1,5 @@
-import { zodResolver } from "@hookform/resolvers/zod";
-import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
+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";
import { useForm } from "react-hook-form";
@@ -54,6 +54,7 @@ const BitbucketProviderSchema = z.object({
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
+ slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
@@ -73,15 +74,16 @@ export const SaveBitbucketProvider = ({ 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: {
owner: "",
repo: "",
+ slug: "",
},
bitbucketId: "",
branch: "",
@@ -114,11 +116,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
} = api.bitbucket.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
- repo: repository?.repo,
+ repo: repository?.slug || repository?.repo || "",
bitbucketId,
},
{
- enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
+ enabled:
+ !!repository?.owner &&
+ !!(repository?.slug || repository?.repo) &&
+ !!bitbucketId,
},
);
@@ -129,6 +134,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
+ slug: data.bitbucketRepositorySlug || "",
},
buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "",
@@ -142,6 +148,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
+ bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketBuildPath: data.buildPath,
bitbucketId: data.bitbucketId,
@@ -181,6 +188,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("repository", {
owner: "",
repo: "",
+ slug: "",
});
form.setValue("branch", "");
}}
@@ -217,7 +225,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
Repository
{field.value.owner && field.value.repo && (
{
!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")}
@@ -255,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.
@@ -271,6 +283,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
+ slug: repo.slug,
});
form.setValue("branch", "");
}}
@@ -320,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(
@@ -337,7 +350,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
- {status === "loading" && fetchStatus === "fetching" && (
+ {status === "pending" && fetchStatus === "fetching" && (
Loading Branches....
@@ -403,10 +416,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
Watch Paths
-
-
- ?
-
+
+
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..37a387bb5 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,5 +1,5 @@
-import { zodResolver } from "@hookform/resolvers/zod";
-import { KeyRoundIcon, LockIcon, X } from "lucide-react";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
+import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
@@ -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: "/",
@@ -228,10 +228,8 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
Watch Paths
-
-
- ?
-
+
+
@@ -317,7 +315,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 d6f65caf3..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: {
@@ -232,9 +232,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
Repository
- {field.value.owner && field.value.repo && (
+ {field.value.gitlabPathNamespace && (
{
!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..01fc9e84a 100644
--- a/apps/dokploy/components/dashboard/application/general/show.tsx
+++ b/apps/dokploy/components/dashboard/application/general/show.tsx
@@ -30,6 +30,9 @@ interface Props {
export const ShowGeneralApplication = ({ applicationId }: Props) => {
const router = useRouter();
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canDeploy = permissions?.deployment.create ?? false;
+ const canUpdateService = permissions?.service.create ?? false;
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
@@ -37,14 +40,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();
@@ -57,128 +60,135 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
- {
- await deploy({
- applicationId: applicationId,
- })
- .then(() => {
- toast.success("Application deployed successfully");
- refetch();
- router.push(
- `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
- );
+ {canDeploy && (
+ {
+ await deploy({
+ applicationId: applicationId,
})
- .catch(() => {
- toast.error("Error deploying application");
- });
- }}
- >
- {
+ toast.success("Application deployed successfully");
+ refetch();
+ router.push(
+ `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
+ );
+ })
+ .catch(() => {
+ toast.error("Error deploying application");
+ });
+ }}
>
-
-
-
-
- Deploy
-
-
-
-
-
- Downloads the source code and performs a complete build
-
-
-
-
-
-
- {
- await reload({
- applicationId: applicationId,
- appName: data?.appName || "",
- })
- .then(() => {
- toast.success("Application reloaded successfully");
- refetch();
+
+
+
+
+
+ Deploy
+
+
+
+
+
+ Downloads the source code and performs a complete
+ build
+
+
+
+
+
+
+ )}
+ {canDeploy && (
+ {
+ await reload({
+ applicationId: applicationId,
+ appName: data?.appName || "",
})
- .catch(() => {
- toast.error("Error reloading application");
- });
- }}
- >
- {
+ toast.success("Application reloaded successfully");
+ refetch();
+ })
+ .catch(() => {
+ toast.error("Error reloading application");
+ });
+ }}
>
-
-
-
-
- Reload
-
-
-
-
- Reload the application without rebuilding it
-
-
-
-
-
- {
- await redeploy({
- applicationId: applicationId,
- })
- .then(() => {
- toast.success("Application rebuilt successfully");
- refetch();
+
+
+
+
+
+ Reload
+
+
+
+
+ Reload the application without rebuilding it
+
+
+
+
+
+ )}
+ {canDeploy && (
+ {
+ await redeploy({
+ applicationId: applicationId,
})
- .catch(() => {
- toast.error("Error rebuilding application");
- });
- }}
- >
- {
+ toast.success("Application rebuilt successfully");
+ refetch();
+ })
+ .catch(() => {
+ toast.error("Error rebuilding application");
+ });
+ }}
>
-
-
-
-
- Rebuild
-
-
-
-
-
- Only rebuilds the application without downloading new
- code
-
-
-
-
-
-
+
+
+
+
+
+ Rebuild
+
+
+
+
+
+ Only rebuilds the application without downloading new
+ code
+
+
+
+
+
+
+ )}
- {data?.applicationStatus === "idle" ? (
+ {canDeploy && data?.applicationStatus === "idle" ? (
{
- ) : (
+ ) : canDeploy ? (
{
- )}
+ ) : null}
{
Open Terminal
-
-
Autodeploy
-
{
- await update({
- applicationId,
- autoDeploy: enabled,
- })
- .then(async () => {
- toast.success("Auto Deploy Updated");
- await refetch();
+ {canUpdateService && (
+
+ Autodeploy
+ {
+ await update({
+ applicationId,
+ autoDeploy: enabled,
})
- .catch(() => {
- toast.error("Error updating Auto Deploy");
- });
- }}
- className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
- />
-
+ .then(async () => {
+ toast.success("Auto Deploy Updated");
+ await refetch();
+ })
+ .catch(() => {
+ toast.error("Error updating Auto Deploy");
+ });
+ }}
+ className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
+ />
+
+ )}
-
-
Clean Cache
-
{
- await update({
- applicationId,
- cleanCache: enabled,
- })
- .then(async () => {
- toast.success("Clean Cache Updated");
- await refetch();
+ {canUpdateService && (
+
+ Clean Cache
+ {
+ await update({
+ applicationId,
+ cleanCache: enabled,
})
- .catch(() => {
- toast.error("Error updating Clean Cache");
- });
- }}
- className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
- />
-
+ .then(async () => {
+ toast.success("Clean Cache Updated");
+ await refetch();
+ })
+ .catch(() => {
+ toast.error("Error updating Clean Cache");
+ });
+ }}
+ className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
+ />
+
+ )}
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 9c2e48931..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
@@ -1,7 +1,9 @@
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
ExternalLink,
FileText,
GitPullRequest,
+ Hammer,
Loader2,
PenSquare,
RocketIcon,
@@ -22,6 +24,12 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
@@ -35,9 +43,12 @@ 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 } =
+ api.previewDeployment.redeploy.useMutation();
+
const {
data: previewDeployments,
refetch: refetchPreviewDeployments,
@@ -46,6 +57,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
+ refetchInterval: 2000,
},
);
@@ -193,6 +205,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
+ {
+ await redeployPreviewDeployment({
+ previewDeploymentId:
+ deployment.previewDeploymentId,
+ })
+ .then(() => {
+ toast.success(
+ "Preview deployment rebuild started",
+ );
+ refetchPreviewDeployments();
+ })
+ .catch(() => {
+ toast.error(
+ "Error rebuilding preview deployment",
+ );
+ });
+ }}
+ >
+
+
+
+
+
+
+ Rebuild
+
+
+
+
+
+ Rebuild the preview deployment without
+ downloading new code
+
+
+
+
+
+
+
+
{
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 32f791029..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 });
@@ -123,7 +123,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewCertificateType: data.previewCertificateType || "none",
previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
- data.previewRequireCollaboratorPermissions || true,
+ data.previewRequireCollaboratorPermissions ?? true,
});
}
}, [data]);
@@ -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 1cb3d34af..36ddb53f1 100644
--- a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx
@@ -1,5 +1,7 @@
-import { zodResolver } from "@hookform/resolvers/zod";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
+ CheckIcon,
+ ChevronsUpDown,
DatabaseZap,
Info,
PenBoxIcon,
@@ -13,6 +15,14 @@ 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 {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
import {
Dialog,
DialogContent,
@@ -31,6 +41,12 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
@@ -48,6 +64,7 @@ import {
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import type { CacheType } from "../domains/handle-domain";
+import { getTimezoneLabel, TIMEZONES } from "./timezones";
export const commonCronExpressions = [
{ label: "Every minute", value: "* * * * *" },
@@ -60,30 +77,6 @@ export const commonCronExpressions = [
{ label: "Custom", value: "custom" },
];
-export const commonTimezones = [
- { label: "UTC (Coordinated Universal Time)", value: "UTC" },
- { label: "America/New_York (Eastern Time)", value: "America/New_York" },
- { label: "America/Chicago (Central Time)", value: "America/Chicago" },
- { label: "America/Denver (Mountain Time)", value: "America/Denver" },
- { label: "America/Los_Angeles (Pacific Time)", value: "America/Los_Angeles" },
- {
- label: "America/Mexico_City (Central Mexico)",
- value: "America/Mexico_City",
- },
- { label: "America/Sao_Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
- { label: "Europe/London (Greenwich Mean Time)", value: "Europe/London" },
- { label: "Europe/Paris (Central European Time)", value: "Europe/Paris" },
- { label: "Europe/Berlin (Central European Time)", value: "Europe/Berlin" },
- { label: "Asia/Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
- { label: "Asia/Shanghai (China Standard Time)", value: "Asia/Shanghai" },
- { label: "Asia/Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
- { label: "Asia/Kolkata (India Standard Time)", value: "Asia/Kolkata" },
- {
- label: "Australia/Sydney (Australian Eastern Time)",
- value: "Australia/Sydney",
- },
-];
-
const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
@@ -227,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: "",
@@ -282,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({
@@ -512,25 +505,60 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
- {
- field.onChange(value);
- }}
- value={field.value}
- >
-
-
-
-
-
-
- {commonTimezones.map((tz) => (
-
- {tz.label}
-
- ))}
-
-
+
+
+
+
+ {getTimezoneLabel(field.value)}
+
+
+
+
+
+
+
+
+ No timezone found.
+
+ {Object.entries(TIMEZONES).map(
+ ([region, zones]) => (
+
+ {zones.map((tz) => (
+ {
+ field.onChange(tz.value);
+ }}
+ >
+ {tz.value}
+
+
+ ))}
+
+ ),
+ )}
+
+
+
+
+
Optional: Choose a timezone for the schedule execution time
@@ -634,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/schedules/timezones.ts b/apps/dokploy/components/dashboard/application/schedules/timezones.ts
new file mode 100644
index 000000000..44891b909
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/schedules/timezones.ts
@@ -0,0 +1,458 @@
+// Complete list of IANA timezones grouped by region
+export const TIMEZONES: Record<
+ string,
+ Array<{ label: string; value: string }>
+> = {
+ Common: [{ label: "UTC (Coordinated Universal Time)", value: "UTC" }],
+ Africa: [
+ { label: "Abidjan", value: "Africa/Abidjan" },
+ { label: "Accra", value: "Africa/Accra" },
+ { label: "Addis Ababa", value: "Africa/Addis_Ababa" },
+ { label: "Algiers", value: "Africa/Algiers" },
+ { label: "Asmara", value: "Africa/Asmara" },
+ { label: "Bamako", value: "Africa/Bamako" },
+ { label: "Bangui", value: "Africa/Bangui" },
+ { label: "Banjul", value: "Africa/Banjul" },
+ { label: "Bissau", value: "Africa/Bissau" },
+ { label: "Blantyre", value: "Africa/Blantyre" },
+ { label: "Brazzaville", value: "Africa/Brazzaville" },
+ { label: "Bujumbura", value: "Africa/Bujumbura" },
+ { label: "Cairo", value: "Africa/Cairo" },
+ { label: "Casablanca", value: "Africa/Casablanca" },
+ { label: "Ceuta", value: "Africa/Ceuta" },
+ { label: "Conakry", value: "Africa/Conakry" },
+ { label: "Dakar", value: "Africa/Dakar" },
+ { label: "Dar es Salaam", value: "Africa/Dar_es_Salaam" },
+ { label: "Djibouti", value: "Africa/Djibouti" },
+ { label: "Douala", value: "Africa/Douala" },
+ { label: "El Aaiun", value: "Africa/El_Aaiun" },
+ { label: "Freetown", value: "Africa/Freetown" },
+ { label: "Gaborone", value: "Africa/Gaborone" },
+ { label: "Harare", value: "Africa/Harare" },
+ { label: "Johannesburg", value: "Africa/Johannesburg" },
+ { label: "Juba", value: "Africa/Juba" },
+ { label: "Kampala", value: "Africa/Kampala" },
+ { label: "Khartoum", value: "Africa/Khartoum" },
+ { label: "Kigali", value: "Africa/Kigali" },
+ { label: "Kinshasa", value: "Africa/Kinshasa" },
+ { label: "Lagos", value: "Africa/Lagos" },
+ { label: "Libreville", value: "Africa/Libreville" },
+ { label: "Lome", value: "Africa/Lome" },
+ { label: "Luanda", value: "Africa/Luanda" },
+ { label: "Lubumbashi", value: "Africa/Lubumbashi" },
+ { label: "Lusaka", value: "Africa/Lusaka" },
+ { label: "Malabo", value: "Africa/Malabo" },
+ { label: "Maputo", value: "Africa/Maputo" },
+ { label: "Maseru", value: "Africa/Maseru" },
+ { label: "Mbabane", value: "Africa/Mbabane" },
+ { label: "Mogadishu", value: "Africa/Mogadishu" },
+ { label: "Monrovia", value: "Africa/Monrovia" },
+ { label: "Nairobi", value: "Africa/Nairobi" },
+ { label: "Ndjamena", value: "Africa/Ndjamena" },
+ { label: "Niamey", value: "Africa/Niamey" },
+ { label: "Nouakchott", value: "Africa/Nouakchott" },
+ { label: "Ouagadougou", value: "Africa/Ouagadougou" },
+ { label: "Porto-Novo", value: "Africa/Porto-Novo" },
+ { label: "Sao Tome", value: "Africa/Sao_Tome" },
+ { label: "Tripoli", value: "Africa/Tripoli" },
+ { label: "Tunis", value: "Africa/Tunis" },
+ { label: "Windhoek", value: "Africa/Windhoek" },
+ ],
+ America: [
+ { label: "Adak", value: "America/Adak" },
+ { label: "Anchorage", value: "America/Anchorage" },
+ { label: "Anguilla", value: "America/Anguilla" },
+ { label: "Antigua", value: "America/Antigua" },
+ { label: "Araguaina", value: "America/Araguaina" },
+ {
+ label: "Argentina/Buenos Aires",
+ value: "America/Argentina/Buenos_Aires",
+ },
+ { label: "Argentina/Catamarca", value: "America/Argentina/Catamarca" },
+ { label: "Argentina/Cordoba", value: "America/Argentina/Cordoba" },
+ { label: "Argentina/Jujuy", value: "America/Argentina/Jujuy" },
+ { label: "Argentina/La Rioja", value: "America/Argentina/La_Rioja" },
+ { label: "Argentina/Mendoza", value: "America/Argentina/Mendoza" },
+ {
+ label: "Argentina/Rio Gallegos",
+ value: "America/Argentina/Rio_Gallegos",
+ },
+ { label: "Argentina/Salta", value: "America/Argentina/Salta" },
+ { label: "Argentina/San Juan", value: "America/Argentina/San_Juan" },
+ { label: "Argentina/San Luis", value: "America/Argentina/San_Luis" },
+ { label: "Argentina/Tucuman", value: "America/Argentina/Tucuman" },
+ { label: "Argentina/Ushuaia", value: "America/Argentina/Ushuaia" },
+ { label: "Aruba", value: "America/Aruba" },
+ { label: "Asuncion", value: "America/Asuncion" },
+ { label: "Atikokan", value: "America/Atikokan" },
+ { label: "Bahia", value: "America/Bahia" },
+ { label: "Bahia Banderas", value: "America/Bahia_Banderas" },
+ { label: "Barbados", value: "America/Barbados" },
+ { label: "Belem", value: "America/Belem" },
+ { label: "Belize", value: "America/Belize" },
+ { label: "Blanc-Sablon", value: "America/Blanc-Sablon" },
+ { label: "Boa Vista", value: "America/Boa_Vista" },
+ { label: "Bogota", value: "America/Bogota" },
+ { label: "Boise", value: "America/Boise" },
+ { label: "Cambridge Bay", value: "America/Cambridge_Bay" },
+ { label: "Campo Grande", value: "America/Campo_Grande" },
+ { label: "Cancun", value: "America/Cancun" },
+ { label: "Caracas", value: "America/Caracas" },
+ { label: "Cayenne", value: "America/Cayenne" },
+ { label: "Cayman", value: "America/Cayman" },
+ { label: "Chicago (Central Time)", value: "America/Chicago" },
+ { label: "Chihuahua", value: "America/Chihuahua" },
+ { label: "Ciudad Juarez", value: "America/Ciudad_Juarez" },
+ { label: "Costa Rica", value: "America/Costa_Rica" },
+ { label: "Creston", value: "America/Creston" },
+ { label: "Cuiaba", value: "America/Cuiaba" },
+ { label: "Curacao", value: "America/Curacao" },
+ { label: "Danmarkshavn", value: "America/Danmarkshavn" },
+ { label: "Dawson", value: "America/Dawson" },
+ { label: "Dawson Creek", value: "America/Dawson_Creek" },
+ { label: "Denver (Mountain Time)", value: "America/Denver" },
+ { label: "Detroit", value: "America/Detroit" },
+ { label: "Dominica", value: "America/Dominica" },
+ { label: "Edmonton", value: "America/Edmonton" },
+ { label: "Eirunepe", value: "America/Eirunepe" },
+ { label: "El Salvador", value: "America/El_Salvador" },
+ { label: "Fort Nelson", value: "America/Fort_Nelson" },
+ { label: "Fortaleza", value: "America/Fortaleza" },
+ { label: "Glace Bay", value: "America/Glace_Bay" },
+ { label: "Goose Bay", value: "America/Goose_Bay" },
+ { label: "Grand Turk", value: "America/Grand_Turk" },
+ { label: "Grenada", value: "America/Grenada" },
+ { label: "Guadeloupe", value: "America/Guadeloupe" },
+ { label: "Guatemala", value: "America/Guatemala" },
+ { label: "Guayaquil", value: "America/Guayaquil" },
+ { label: "Guyana", value: "America/Guyana" },
+ { label: "Halifax", value: "America/Halifax" },
+ { label: "Havana", value: "America/Havana" },
+ { label: "Hermosillo", value: "America/Hermosillo" },
+ { label: "Indiana/Indianapolis", value: "America/Indiana/Indianapolis" },
+ { label: "Indiana/Knox", value: "America/Indiana/Knox" },
+ { label: "Indiana/Marengo", value: "America/Indiana/Marengo" },
+ { label: "Indiana/Petersburg", value: "America/Indiana/Petersburg" },
+ { label: "Indiana/Tell City", value: "America/Indiana/Tell_City" },
+ { label: "Indiana/Vevay", value: "America/Indiana/Vevay" },
+ { label: "Indiana/Vincennes", value: "America/Indiana/Vincennes" },
+ { label: "Indiana/Winamac", value: "America/Indiana/Winamac" },
+ { label: "Inuvik", value: "America/Inuvik" },
+ { label: "Iqaluit", value: "America/Iqaluit" },
+ { label: "Jamaica", value: "America/Jamaica" },
+ { label: "Juneau", value: "America/Juneau" },
+ { label: "Kentucky/Louisville", value: "America/Kentucky/Louisville" },
+ { label: "Kentucky/Monticello", value: "America/Kentucky/Monticello" },
+ { label: "Kralendijk", value: "America/Kralendijk" },
+ { label: "La Paz", value: "America/La_Paz" },
+ { label: "Lima", value: "America/Lima" },
+ { label: "Los Angeles (Pacific Time)", value: "America/Los_Angeles" },
+ { label: "Lower Princes", value: "America/Lower_Princes" },
+ { label: "Maceio", value: "America/Maceio" },
+ { label: "Managua", value: "America/Managua" },
+ { label: "Manaus", value: "America/Manaus" },
+ { label: "Marigot", value: "America/Marigot" },
+ { label: "Martinique", value: "America/Martinique" },
+ { label: "Matamoros", value: "America/Matamoros" },
+ { label: "Mazatlan", value: "America/Mazatlan" },
+ { label: "Menominee", value: "America/Menominee" },
+ { label: "Merida", value: "America/Merida" },
+ { label: "Metlakatla", value: "America/Metlakatla" },
+ { label: "Mexico City (Central Mexico)", value: "America/Mexico_City" },
+ { label: "Miquelon", value: "America/Miquelon" },
+ { label: "Moncton", value: "America/Moncton" },
+ { label: "Monterrey", value: "America/Monterrey" },
+ { label: "Montevideo", value: "America/Montevideo" },
+ { label: "Montserrat", value: "America/Montserrat" },
+ { label: "Nassau", value: "America/Nassau" },
+ { label: "New York (Eastern Time)", value: "America/New_York" },
+ { label: "Nome", value: "America/Nome" },
+ { label: "Noronha", value: "America/Noronha" },
+ { label: "North Dakota/Beulah", value: "America/North_Dakota/Beulah" },
+ { label: "North Dakota/Center", value: "America/North_Dakota/Center" },
+ {
+ label: "North Dakota/New Salem",
+ value: "America/North_Dakota/New_Salem",
+ },
+ { label: "Nuuk", value: "America/Nuuk" },
+ { label: "Ojinaga", value: "America/Ojinaga" },
+ { label: "Panama", value: "America/Panama" },
+ { label: "Paramaribo", value: "America/Paramaribo" },
+ { label: "Phoenix", value: "America/Phoenix" },
+ { label: "Port-au-Prince", value: "America/Port-au-Prince" },
+ { label: "Port of Spain", value: "America/Port_of_Spain" },
+ { label: "Porto Velho", value: "America/Porto_Velho" },
+ { label: "Puerto Rico", value: "America/Puerto_Rico" },
+ { label: "Punta Arenas", value: "America/Punta_Arenas" },
+ { label: "Rankin Inlet", value: "America/Rankin_Inlet" },
+ { label: "Recife", value: "America/Recife" },
+ { label: "Regina", value: "America/Regina" },
+ { label: "Resolute", value: "America/Resolute" },
+ { label: "Rio Branco", value: "America/Rio_Branco" },
+ { label: "Santarem", value: "America/Santarem" },
+ { label: "Santiago", value: "America/Santiago" },
+ { label: "Santo Domingo", value: "America/Santo_Domingo" },
+ { label: "Sao Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
+ { label: "Scoresbysund", value: "America/Scoresbysund" },
+ { label: "Sitka", value: "America/Sitka" },
+ { label: "St Barthelemy", value: "America/St_Barthelemy" },
+ { label: "St Johns", value: "America/St_Johns" },
+ { label: "St Kitts", value: "America/St_Kitts" },
+ { label: "St Lucia", value: "America/St_Lucia" },
+ { label: "St Thomas", value: "America/St_Thomas" },
+ { label: "St Vincent", value: "America/St_Vincent" },
+ { label: "Swift Current", value: "America/Swift_Current" },
+ { label: "Tegucigalpa", value: "America/Tegucigalpa" },
+ { label: "Thule", value: "America/Thule" },
+ { label: "Tijuana", value: "America/Tijuana" },
+ { label: "Toronto", value: "America/Toronto" },
+ { label: "Tortola", value: "America/Tortola" },
+ { label: "Vancouver", value: "America/Vancouver" },
+ { label: "Whitehorse", value: "America/Whitehorse" },
+ { label: "Winnipeg", value: "America/Winnipeg" },
+ { label: "Yakutat", value: "America/Yakutat" },
+ ],
+ Antarctica: [
+ { label: "Casey", value: "Antarctica/Casey" },
+ { label: "Davis", value: "Antarctica/Davis" },
+ { label: "DumontDUrville", value: "Antarctica/DumontDUrville" },
+ { label: "Macquarie", value: "Antarctica/Macquarie" },
+ { label: "Mawson", value: "Antarctica/Mawson" },
+ { label: "McMurdo", value: "Antarctica/McMurdo" },
+ { label: "Palmer", value: "Antarctica/Palmer" },
+ { label: "Rothera", value: "Antarctica/Rothera" },
+ { label: "Syowa", value: "Antarctica/Syowa" },
+ { label: "Troll", value: "Antarctica/Troll" },
+ { label: "Vostok", value: "Antarctica/Vostok" },
+ ],
+ Arctic: [{ label: "Longyearbyen", value: "Arctic/Longyearbyen" }],
+ Asia: [
+ { label: "Aden", value: "Asia/Aden" },
+ { label: "Almaty", value: "Asia/Almaty" },
+ { label: "Amman", value: "Asia/Amman" },
+ { label: "Anadyr", value: "Asia/Anadyr" },
+ { label: "Aqtau", value: "Asia/Aqtau" },
+ { label: "Aqtobe", value: "Asia/Aqtobe" },
+ { label: "Ashgabat", value: "Asia/Ashgabat" },
+ { label: "Atyrau", value: "Asia/Atyrau" },
+ { label: "Baghdad", value: "Asia/Baghdad" },
+ { label: "Bahrain", value: "Asia/Bahrain" },
+ { label: "Baku", value: "Asia/Baku" },
+ { label: "Bangkok", value: "Asia/Bangkok" },
+ { label: "Barnaul", value: "Asia/Barnaul" },
+ { label: "Beirut", value: "Asia/Beirut" },
+ { label: "Bishkek", value: "Asia/Bishkek" },
+ { label: "Brunei", value: "Asia/Brunei" },
+ { label: "Chita", value: "Asia/Chita" },
+ { label: "Choibalsan", value: "Asia/Choibalsan" },
+ { label: "Colombo", value: "Asia/Colombo" },
+ { label: "Damascus", value: "Asia/Damascus" },
+ { label: "Dhaka", value: "Asia/Dhaka" },
+ { label: "Dili", value: "Asia/Dili" },
+ { label: "Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
+ { label: "Dushanbe", value: "Asia/Dushanbe" },
+ { label: "Famagusta", value: "Asia/Famagusta" },
+ { label: "Gaza", value: "Asia/Gaza" },
+ { label: "Hebron", value: "Asia/Hebron" },
+ { label: "Ho Chi Minh", value: "Asia/Ho_Chi_Minh" },
+ { label: "Hong Kong", value: "Asia/Hong_Kong" },
+ { label: "Hovd", value: "Asia/Hovd" },
+ { label: "Irkutsk", value: "Asia/Irkutsk" },
+ { label: "Jakarta", value: "Asia/Jakarta" },
+ { label: "Jayapura", value: "Asia/Jayapura" },
+ { label: "Jerusalem", value: "Asia/Jerusalem" },
+ { label: "Kabul", value: "Asia/Kabul" },
+ { label: "Kamchatka", value: "Asia/Kamchatka" },
+ { label: "Karachi", value: "Asia/Karachi" },
+ { label: "Kathmandu", value: "Asia/Kathmandu" },
+ { label: "Khandyga", value: "Asia/Khandyga" },
+ { label: "Kolkata (India Standard Time)", value: "Asia/Kolkata" },
+ { label: "Krasnoyarsk", value: "Asia/Krasnoyarsk" },
+ { label: "Kuala Lumpur", value: "Asia/Kuala_Lumpur" },
+ { label: "Kuching", value: "Asia/Kuching" },
+ { label: "Kuwait", value: "Asia/Kuwait" },
+ { label: "Macau", value: "Asia/Macau" },
+ { label: "Magadan", value: "Asia/Magadan" },
+ { label: "Makassar", value: "Asia/Makassar" },
+ { label: "Manila", value: "Asia/Manila" },
+ { label: "Muscat", value: "Asia/Muscat" },
+ { label: "Nicosia", value: "Asia/Nicosia" },
+ { label: "Novokuznetsk", value: "Asia/Novokuznetsk" },
+ { label: "Novosibirsk", value: "Asia/Novosibirsk" },
+ { label: "Omsk", value: "Asia/Omsk" },
+ { label: "Oral", value: "Asia/Oral" },
+ { label: "Phnom Penh", value: "Asia/Phnom_Penh" },
+ { label: "Pontianak", value: "Asia/Pontianak" },
+ { label: "Pyongyang", value: "Asia/Pyongyang" },
+ { label: "Qatar", value: "Asia/Qatar" },
+ { label: "Qostanay", value: "Asia/Qostanay" },
+ { label: "Qyzylorda", value: "Asia/Qyzylorda" },
+ { label: "Riyadh", value: "Asia/Riyadh" },
+ { label: "Sakhalin", value: "Asia/Sakhalin" },
+ { label: "Samarkand", value: "Asia/Samarkand" },
+ { label: "Seoul", value: "Asia/Seoul" },
+ { label: "Shanghai (China Standard Time)", value: "Asia/Shanghai" },
+ { label: "Singapore", value: "Asia/Singapore" },
+ { label: "Srednekolymsk", value: "Asia/Srednekolymsk" },
+ { label: "Taipei", value: "Asia/Taipei" },
+ { label: "Tashkent", value: "Asia/Tashkent" },
+ { label: "Tbilisi", value: "Asia/Tbilisi" },
+ { label: "Tehran", value: "Asia/Tehran" },
+ { label: "Thimphu", value: "Asia/Thimphu" },
+ { label: "Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
+ { label: "Tomsk", value: "Asia/Tomsk" },
+ { label: "Ulaanbaatar", value: "Asia/Ulaanbaatar" },
+ { label: "Urumqi", value: "Asia/Urumqi" },
+ { label: "Ust-Nera", value: "Asia/Ust-Nera" },
+ { label: "Vientiane", value: "Asia/Vientiane" },
+ { label: "Vladivostok", value: "Asia/Vladivostok" },
+ { label: "Yakutsk", value: "Asia/Yakutsk" },
+ { label: "Yangon", value: "Asia/Yangon" },
+ { label: "Yekaterinburg", value: "Asia/Yekaterinburg" },
+ { label: "Yerevan", value: "Asia/Yerevan" },
+ ],
+ Atlantic: [
+ { label: "Azores", value: "Atlantic/Azores" },
+ { label: "Bermuda", value: "Atlantic/Bermuda" },
+ { label: "Canary", value: "Atlantic/Canary" },
+ { label: "Cape Verde", value: "Atlantic/Cape_Verde" },
+ { label: "Faroe", value: "Atlantic/Faroe" },
+ { label: "Madeira", value: "Atlantic/Madeira" },
+ { label: "Reykjavik", value: "Atlantic/Reykjavik" },
+ { label: "South Georgia", value: "Atlantic/South_Georgia" },
+ { label: "St Helena", value: "Atlantic/St_Helena" },
+ { label: "Stanley", value: "Atlantic/Stanley" },
+ ],
+ Australia: [
+ { label: "Adelaide", value: "Australia/Adelaide" },
+ { label: "Brisbane", value: "Australia/Brisbane" },
+ { label: "Broken Hill", value: "Australia/Broken_Hill" },
+ { label: "Darwin", value: "Australia/Darwin" },
+ { label: "Eucla", value: "Australia/Eucla" },
+ { label: "Hobart", value: "Australia/Hobart" },
+ { label: "Lindeman", value: "Australia/Lindeman" },
+ { label: "Lord Howe", value: "Australia/Lord_Howe" },
+ { label: "Melbourne", value: "Australia/Melbourne" },
+ { label: "Perth", value: "Australia/Perth" },
+ { label: "Sydney (Australian Eastern Time)", value: "Australia/Sydney" },
+ ],
+ Europe: [
+ { label: "Amsterdam", value: "Europe/Amsterdam" },
+ { label: "Andorra", value: "Europe/Andorra" },
+ { label: "Astrakhan", value: "Europe/Astrakhan" },
+ { label: "Athens", value: "Europe/Athens" },
+ { label: "Belgrade", value: "Europe/Belgrade" },
+ { label: "Berlin (Central European Time)", value: "Europe/Berlin" },
+ { label: "Bratislava", value: "Europe/Bratislava" },
+ { label: "Brussels", value: "Europe/Brussels" },
+ { label: "Bucharest", value: "Europe/Bucharest" },
+ { label: "Budapest", value: "Europe/Budapest" },
+ { label: "Busingen", value: "Europe/Busingen" },
+ { label: "Chisinau", value: "Europe/Chisinau" },
+ { label: "Copenhagen", value: "Europe/Copenhagen" },
+ { label: "Dublin", value: "Europe/Dublin" },
+ { label: "Gibraltar", value: "Europe/Gibraltar" },
+ { label: "Guernsey", value: "Europe/Guernsey" },
+ { label: "Helsinki", value: "Europe/Helsinki" },
+ { label: "Isle of Man", value: "Europe/Isle_of_Man" },
+ { label: "Istanbul", value: "Europe/Istanbul" },
+ { label: "Jersey", value: "Europe/Jersey" },
+ { label: "Kaliningrad", value: "Europe/Kaliningrad" },
+ { label: "Kirov", value: "Europe/Kirov" },
+ { label: "Kyiv", value: "Europe/Kyiv" },
+ { label: "Lisbon", value: "Europe/Lisbon" },
+ { label: "Ljubljana", value: "Europe/Ljubljana" },
+ { label: "London (Greenwich Mean Time)", value: "Europe/London" },
+ { label: "Luxembourg", value: "Europe/Luxembourg" },
+ { label: "Madrid", value: "Europe/Madrid" },
+ { label: "Malta", value: "Europe/Malta" },
+ { label: "Mariehamn", value: "Europe/Mariehamn" },
+ { label: "Minsk", value: "Europe/Minsk" },
+ { label: "Monaco", value: "Europe/Monaco" },
+ { label: "Moscow", value: "Europe/Moscow" },
+ { label: "Oslo", value: "Europe/Oslo" },
+ { label: "Paris (Central European Time)", value: "Europe/Paris" },
+ { label: "Podgorica", value: "Europe/Podgorica" },
+ { label: "Prague", value: "Europe/Prague" },
+ { label: "Riga", value: "Europe/Riga" },
+ { label: "Rome", value: "Europe/Rome" },
+ { label: "Samara", value: "Europe/Samara" },
+ { label: "San Marino", value: "Europe/San_Marino" },
+ { label: "Sarajevo", value: "Europe/Sarajevo" },
+ { label: "Saratov", value: "Europe/Saratov" },
+ { label: "Simferopol", value: "Europe/Simferopol" },
+ { label: "Skopje", value: "Europe/Skopje" },
+ { label: "Sofia", value: "Europe/Sofia" },
+ { label: "Stockholm", value: "Europe/Stockholm" },
+ { label: "Tallinn", value: "Europe/Tallinn" },
+ { label: "Tirane", value: "Europe/Tirane" },
+ { label: "Ulyanovsk", value: "Europe/Ulyanovsk" },
+ { label: "Vaduz", value: "Europe/Vaduz" },
+ { label: "Vatican", value: "Europe/Vatican" },
+ { label: "Vienna", value: "Europe/Vienna" },
+ { label: "Vilnius", value: "Europe/Vilnius" },
+ { label: "Volgograd", value: "Europe/Volgograd" },
+ { label: "Warsaw", value: "Europe/Warsaw" },
+ { label: "Zagreb", value: "Europe/Zagreb" },
+ { label: "Zurich", value: "Europe/Zurich" },
+ ],
+ Indian: [
+ { label: "Antananarivo", value: "Indian/Antananarivo" },
+ { label: "Chagos", value: "Indian/Chagos" },
+ { label: "Christmas", value: "Indian/Christmas" },
+ { label: "Cocos", value: "Indian/Cocos" },
+ { label: "Comoro", value: "Indian/Comoro" },
+ { label: "Kerguelen", value: "Indian/Kerguelen" },
+ { label: "Mahe", value: "Indian/Mahe" },
+ { label: "Maldives", value: "Indian/Maldives" },
+ { label: "Mauritius", value: "Indian/Mauritius" },
+ { label: "Mayotte", value: "Indian/Mayotte" },
+ { label: "Reunion", value: "Indian/Reunion" },
+ ],
+ Pacific: [
+ { label: "Apia", value: "Pacific/Apia" },
+ { label: "Auckland", value: "Pacific/Auckland" },
+ { label: "Bougainville", value: "Pacific/Bougainville" },
+ { label: "Chatham", value: "Pacific/Chatham" },
+ { label: "Chuuk", value: "Pacific/Chuuk" },
+ { label: "Easter", value: "Pacific/Easter" },
+ { label: "Efate", value: "Pacific/Efate" },
+ { label: "Fakaofo", value: "Pacific/Fakaofo" },
+ { label: "Fiji", value: "Pacific/Fiji" },
+ { label: "Funafuti", value: "Pacific/Funafuti" },
+ { label: "Galapagos", value: "Pacific/Galapagos" },
+ { label: "Gambier", value: "Pacific/Gambier" },
+ { label: "Guadalcanal", value: "Pacific/Guadalcanal" },
+ { label: "Guam", value: "Pacific/Guam" },
+ { label: "Honolulu", value: "Pacific/Honolulu" },
+ { label: "Kanton", value: "Pacific/Kanton" },
+ { label: "Kiritimati", value: "Pacific/Kiritimati" },
+ { label: "Kosrae", value: "Pacific/Kosrae" },
+ { label: "Kwajalein", value: "Pacific/Kwajalein" },
+ { label: "Majuro", value: "Pacific/Majuro" },
+ { label: "Marquesas", value: "Pacific/Marquesas" },
+ { label: "Midway", value: "Pacific/Midway" },
+ { label: "Nauru", value: "Pacific/Nauru" },
+ { label: "Niue", value: "Pacific/Niue" },
+ { label: "Norfolk", value: "Pacific/Norfolk" },
+ { label: "Noumea", value: "Pacific/Noumea" },
+ { label: "Pago Pago", value: "Pacific/Pago_Pago" },
+ { label: "Palau", value: "Pacific/Palau" },
+ { label: "Pitcairn", value: "Pacific/Pitcairn" },
+ { label: "Pohnpei", value: "Pacific/Pohnpei" },
+ { label: "Port Moresby", value: "Pacific/Port_Moresby" },
+ { label: "Rarotonga", value: "Pacific/Rarotonga" },
+ { label: "Saipan", value: "Pacific/Saipan" },
+ { label: "Tahiti", value: "Pacific/Tahiti" },
+ { label: "Tarawa", value: "Pacific/Tarawa" },
+ { label: "Tongatapu", value: "Pacific/Tongatapu" },
+ { label: "Wake", value: "Pacific/Wake" },
+ { label: "Wallis", value: "Pacific/Wallis" },
+ ],
+};
+
+// Helper to get display label for a timezone value
+export function getTimezoneLabel(value: string | undefined): string {
+ if (!value) return "UTC (default)";
+ return value;
+}
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..0d87080d7 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";
@@ -71,6 +71,7 @@ const formSchema = z
"mongo",
"mysql",
"redis",
+ "libsql",
]),
serviceName: z.string(),
destinationId: z.string().min(1, "Destination required"),
@@ -116,7 +117,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 +196,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 +208,7 @@ export const HandleVolumeBackups = ({
await mutateAsync({
...values,
- keepLatestCount: preparedKeepLatestCount,
+ keepLatestCount: preparedKeepLatestCount ?? undefined,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
@@ -630,7 +631,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..35fe01ff9 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";
@@ -46,6 +46,8 @@ interface Props {
}
export const DeleteService = ({ id, type }: Props) => {
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canDelete = permissions?.service.delete ?? false;
const [isOpen, setIsOpen] = useState(false);
const queryMap = {
@@ -55,6 +57,7 @@ export const DeleteService = ({ id, type }: Props) => {
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
+ libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
@@ -70,11 +73,12 @@ export const DeleteService = ({ id, type }: Props) => {
redis: () => api.redis.remove.useMutation(),
mysql: () => api.mysql.remove.useMutation(),
mariadb: () => api.mariadb.remove.useMutation(),
+ libsql: () => api.libsql.remove.useMutation(),
application: () => api.application.delete.useMutation(),
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();
@@ -96,6 +100,7 @@ export const DeleteService = ({ id, type }: Props) => {
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
+ libsqlId: id || "",
applicationId: id || "",
composeId: id || "",
deleteVolumes,
@@ -123,6 +128,8 @@ export const DeleteService = ({ id, type }: Props) => {
data?.applicationStatus === "running") ||
(data && "composeStatus" in data && data?.composeStatus === "running");
+ if (!canDelete) return null;
+
return (
@@ -130,7 +137,7 @@ export const DeleteService = ({ id, type }: Props) => {
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
- isLoading={isLoading}
+ isLoading={isPending}
>
@@ -228,7 +235,7 @@ export const DeleteService = ({ id, type }: Props) => {
{
const router = useRouter();
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canDeploy = permissions?.deployment.create ?? false;
+ const canUpdateService = permissions?.service.create ?? false;
const { data, refetch } = api.compose.one.useQuery(
{
composeId,
@@ -28,169 +31,176 @@ export const ComposeActions = ({ composeId }: 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 (
- {
- await deploy({
- composeId: composeId,
- })
- .then(() => {
- toast.success("Compose deployed successfully");
- refetch();
- router.push(
- `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
- );
- })
- .catch(() => {
- toast.error("Error deploying compose");
- });
- }}
- >
-
-
-
-
-
- Deploy
-
-
-
-
- Downloads the source code and performs a complete build
-
-
-
-
-
- {
- await redeploy({
- composeId: composeId,
- })
- .then(() => {
- toast.success("Compose reloaded successfully");
- refetch();
- })
- .catch(() => {
- toast.error("Error reloading compose");
- });
- }}
- >
-
-
-
-
-
- Reload
-
-
-
-
- Reload the compose without rebuilding it
-
-
-
-
-
- {data?.composeType === "docker-compose" &&
- data?.composeStatus === "idle" ? (
+ {canDeploy && (
{
- await start({
+ await deploy({
composeId: composeId,
})
.then(() => {
- toast.success("Compose started successfully");
+ toast.success("Compose deployed successfully");
refetch();
+ router.push(
+ `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
+ );
})
.catch(() => {
- toast.error("Error starting compose");
+ toast.error("Error deploying compose");
});
}}
>
-
- Start
+
+ Deploy
- Start the compose (requires a previous successful build)
+ Downloads the source code and performs a complete build
- ) : (
+ )}
+ {canDeploy && (
{
- await stop({
+ await redeploy({
composeId: composeId,
})
.then(() => {
- toast.success("Compose stopped successfully");
+ toast.success("Compose reloaded successfully");
refetch();
})
.catch(() => {
- toast.error("Error stopping compose");
+ toast.error("Error reloading compose");
});
}}
>
-
- Stop
+
+ Reload
- Stop the currently running compose
+ Reload the compose without rebuilding it
)}
+ {canDeploy &&
+ (data?.composeType === "docker-compose" &&
+ data?.composeStatus === "idle" ? (
+ {
+ await start({
+ composeId: composeId,
+ })
+ .then(() => {
+ toast.success("Compose started successfully");
+ refetch();
+ })
+ .catch(() => {
+ toast.error("Error starting compose");
+ });
+ }}
+ >
+
+
+
+
+
+ Start
+
+
+
+
+
+ Start the compose (requires a previous successful build)
+
+
+
+
+
+
+ ) : (
+ {
+ await stop({
+ composeId: composeId,
+ })
+ .then(() => {
+ toast.success("Compose stopped successfully");
+ refetch();
+ })
+ .catch(() => {
+ toast.error("Error stopping compose");
+ });
+ }}
+ >
+
+
+
+
+
+ Stop
+
+
+
+
+ Stop the currently running compose
+
+
+
+
+
+ ))}
{
Open Terminal
-
-
Autodeploy
-
{
- await update({
- composeId,
- autoDeploy: enabled,
- })
- .then(async () => {
- toast.success("Auto Deploy Updated");
- await refetch();
+ {canUpdateService && (
+
+ Autodeploy
+ {
+ await update({
+ composeId,
+ autoDeploy: enabled,
})
- .catch(() => {
- toast.error("Error updating Auto Deploy");
- });
- }}
- className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
- />
-
+ .then(async () => {
+ toast.success("Auto Deploy Updated");
+ await refetch();
+ })
+ .catch(() => {
+ toast.error("Error updating Auto Deploy");
+ });
+ }}
+ className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
+ />
+
+ )}
);
};
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..28f958e3e 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";
@@ -26,6 +26,8 @@ const AddComposeFile = z.object({
type AddComposeFile = z.infer;
export const ComposeFileEditor = ({ composeId }: Props) => {
+ const { data: permissions } = api.user.getPermissions.useQuery();
+ const canUpdate = permissions?.service.create ?? false;
const utils = api.useUtils();
const { data, refetch } = api.compose.one.useQuery(
{
@@ -34,7 +36,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 +95,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 +105,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [form, onSubmit, isLoading]);
+ }, [form, onSubmit, isPending]);
return (
<>
@@ -164,14 +166,16 @@ services:
-
- Save
-
+ {canUpdate && (
+
+ 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 06c88fff4..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";
@@ -54,6 +54,7 @@ const BitbucketProviderSchema = z.object({
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
+ slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
@@ -73,15 +74,16 @@ 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: {
owner: "",
repo: "",
+ slug: "",
},
bitbucketId: "",
branch: "",
@@ -114,11 +116,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
} = api.bitbucket.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
- repo: repository?.repo,
+ repo: repository?.slug || repository?.repo || "",
bitbucketId,
},
{
- enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
+ enabled:
+ !!repository?.owner &&
+ !!(repository?.slug || repository?.repo) &&
+ !!bitbucketId,
},
);
@@ -129,6 +134,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
+ slug: data.bitbucketRepositorySlug || "",
},
composePath: data.composePath,
bitbucketId: data.bitbucketId || "",
@@ -142,6 +148,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
+ bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketId: data.bitbucketId,
composePath: data.composePath,
@@ -183,6 +190,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("repository", {
owner: "",
repo: "",
+ slug: "",
});
form.setValue("branch", "");
}}
@@ -219,7 +227,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
Repository
{field.value.owner && field.value.repo && (
{
!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")}
@@ -257,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.
@@ -273,6 +285,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
+ slug: repo.slug,
});
form.setValue("branch", "");
}}
@@ -322,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(
@@ -339,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..c84a55bb3 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,5 +1,5 @@
-import { zodResolver } from "@hookform/resolvers/zod";
-import { KeyRoundIcon, LockIcon, X } from "lucide-react";
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
+import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
@@ -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: "",
@@ -230,10 +230,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
Watch Paths
-
-
- ?
-
+
+
@@ -318,7 +316,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {