Compare commits

..

9 Commits

Author SHA1 Message Date
Mauricio Siu
df3965a581 Merge pull request #4800 from EvanSchleret/fix/typo
fix(ui): typos
2026-07-14 23:44:00 -06:00
Mauricio Siu
bddc0c3d15 Merge pull request #4825 from Dokploy/fix/allow-clearing-server-domain
fix(settings): allow clearing the server domain
2026-07-14 15:20:27 -06:00
Mauricio Siu
9626c162cc Merge pull request #4826 from Dokploy/fix/repo-selector-duplicate-names
fix(ui): disambiguate repos with the same name in the repository selector
2026-07-14 15:18:47 -06:00
Mauricio Siu
c4596ffa76 refactor(ui): improve repository and branch display in popovers
Updated the PopoverContent components across various provider files to ensure consistent width and padding. Enhanced the display of repository and branch names by adding truncation to prevent overflow in the UI. This change applies to Bitbucket, Gitea, GitHub, and GitLab provider components, as well as their respective compose components.
2026-07-14 15:16:56 -06:00
Mauricio Siu
de62aff0fb fix(ui): disambiguate repos with the same name in the repository selector
The repository CommandItem used the repo name as its cmdk value and
the check icon compared only names, so two repos with the same name in
different owners/orgs showed the selected checkmark and hover on both
entries. Key the item by owner/name and include the owner in the
selected comparison. GitLab already keyed by URL and is unaffected.

Fixes #4793
2026-07-14 11:42:31 -06:00
Mauricio Siu
d5dd35c8f8 fix(settings): allow clearing the server domain
The Server Domain form rejected an empty value ('Invalid domain name'),
making domain assignment a one-way operation. The backend already
removes the Traefik router and clears the host when it receives an
empty host, so only the client-side validation blocked removal.

Allow an empty domain to clear it, and skip the https/letsencrypt
requirements when the domain is being removed.

Fixes #4821
2026-07-14 11:39:30 -06:00
Mauricio Siu
4631ede015 Merge pull request #4823 from Dokploy/fix/compose-volume-suffix-access-mode
fix(compose): preserve named-volume access mode when adding suffix
2026-07-14 11:38:10 -06:00
Mauricio Siu
bc22d05f8d fix(compose): preserve named-volume access mode when adding suffix
The randomize/isolated-deployment volume transform split mount strings
on ':' and kept only the first two segments, so an access mode like
:ro, :z or :Z was silently dropped and read-only mounts became
read-write. Keep the full path+mode remainder when rebuilding the
mount string.

Fixes #4818
2026-07-14 11:34:46 -06:00
Evan Schleret
12d3f1871c fix(ui): typos 2026-07-12 03:13:52 +02:00
17 changed files with 198 additions and 121 deletions

View File

@@ -42,6 +42,39 @@ test("Add suffix to volumes declared directly in services", () => {
); );
}); });
const composeFileAccessMode = `
version: "3.8"
services:
web:
image: nginx:alpine
volumes:
- web_config:/etc/nginx/conf.d:ro
- certs/sub:/etc/certs:Z
`;
test("Add suffix to volumes preserves access mode (:ro, :z, :Z)", () => {
const composeData = parse(composeFileAccessMode) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addSuffixToVolumesInServices(
composeData.services,
suffix,
);
expect(updatedComposeData.web?.volumes).toContain(
`web_config-${suffix}:/etc/nginx/conf.d:ro`,
);
expect(updatedComposeData.web?.volumes).toContain(
`certs-${suffix}/sub:/etc/certs:Z`,
);
});
const composeFileTypeVolume = ` const composeFileTypeVolume = `
version: "3.8" version: "3.8"

View File

@@ -1,31 +0,0 @@
import { getLogType } from "@/components/dashboard/docker/logs/utils";
import { expect, test } from "vitest";
test("classifies real failures as error", () => {
expect(getLogType("Error: connection refused at db:5432").type).toBe("error");
expect(getLogType("[ERROR] something went wrong").type).toBe("error");
expect(getLogType("Deployment failed").type).toBe("error");
expect(
getLogType(
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326ms", failed: true, skipped: false, error: exit code 1',
).type,
).toBe("error");
});
test("does not classify explicit non-error key/values as error (#4538)", () => {
// ofelia job-completion summary for a successful run
expect(
getLogType(
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326.16795ms", failed: false, skipped: false, error: none',
).type,
).not.toBe("error");
expect(getLogType("request done, error: null").type).not.toBe("error");
expect(getLogType("checks passed, failures=0").type).not.toBe("error");
expect(getLogType('shutdown clean, error=""').type).not.toBe("error");
});
test("keeps statusCode-based classification", () => {
expect(getLogType('{"statusCode": "500"}').type).toBe("error");
expect(getLogType('{"statusCode": "204"}').type).toBe("success");
});

View File

@@ -256,14 +256,19 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
: isLoadingRepositories : isLoadingRepositories
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) =>
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")} )?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -283,7 +288,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CommandGroup> <CommandGroup>
{repositories?.map((repo) => ( {repositories?.map((repo) => (
<CommandItem <CommandItem
value={repo.name} value={`${repo.owner.username}/${repo.name}`}
key={repo.url} key={repo.url}
onSelect={() => { onSelect={() => {
form.setValue("repository", { form.setValue("repository", {
@@ -294,8 +299,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.username} {repo.owner.username}
</span> </span>
@@ -303,7 +308,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
repo.name === field.value.repo repo.name === field.value.repo &&
repo.owner.username === field.value.owner
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
@@ -350,7 +356,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -378,7 +387,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -270,14 +270,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo: GiteaRepository) => (repo: GiteaRepository) =>
repo.name === field.value.repo, repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")} )?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -303,7 +307,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
{repositories?.map((repo: GiteaRepository) => { {repositories?.map((repo: GiteaRepository) => {
return ( return (
<CommandItem <CommandItem
value={repo.name} value={`${repo.owner.username}/${repo.name}`}
key={repo.url} key={repo.url}
onSelect={() => { onSelect={() => {
form.setValue("repository", { form.setValue("repository", {
@@ -313,8 +317,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">
{repo.name}
</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.username} {repo.owner.username}
</span> </span>
@@ -322,7 +328,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
repo.name === field.value.repo repo.name === field.value.repo &&
repo.owner.username ===
field.value.owner
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
@@ -371,7 +379,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -402,7 +413,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -252,14 +252,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
: isLoadingRepositories : isLoadingRepositories
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) =>
repo.name === field.value.repo &&
repo.owner.login === field.value.owner,
)?.name ?? field.value.repo)} )?.name ?? field.value.repo)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -279,7 +284,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CommandGroup> <CommandGroup>
{repositories?.map((repo) => ( {repositories?.map((repo) => (
<CommandItem <CommandItem
value={repo.name} value={`${repo.owner.login}/${repo.name}`}
key={repo.url} key={repo.url}
onSelect={() => { onSelect={() => {
form.setValue("repository", { form.setValue("repository", {
@@ -289,8 +294,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.login} {repo.owner.login}
</span> </span>
@@ -298,7 +303,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
repo.name === field.value.repo repo.name === field.value.repo &&
repo.owner.login === field.value.owner
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
@@ -345,7 +351,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -373,7 +382,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -272,7 +272,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -310,8 +313,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">
{repo.name}
</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.username} {repo.owner.username}
</span> </span>
@@ -368,7 +373,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -396,7 +404,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -258,14 +258,19 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
: isLoadingRepositories : isLoadingRepositories
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) =>
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")} )?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -285,7 +290,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CommandGroup> <CommandGroup>
{repositories?.map((repo) => ( {repositories?.map((repo) => (
<CommandItem <CommandItem
value={repo.name} value={`${repo.owner.username}/${repo.name}`}
key={repo.url} key={repo.url}
onSelect={() => { onSelect={() => {
form.setValue("repository", { form.setValue("repository", {
@@ -296,8 +301,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.username} {repo.owner.username}
</span> </span>
@@ -305,7 +310,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
repo.name === field.value.repo repo.name === field.value.repo &&
repo.owner.username === field.value.owner
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
@@ -352,7 +358,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -380,7 +389,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -255,13 +255,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
: isLoadingRepositories : isLoadingRepositories
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) =>
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")} )?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -282,7 +287,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
{repositories?.map((repo) => ( {repositories?.map((repo) => (
<CommandItem <CommandItem
key={repo.url} key={repo.url}
value={repo.name} value={`${repo.owner.username}/${repo.name}`}
onSelect={() => { onSelect={() => {
form.setValue("repository", { form.setValue("repository", {
owner: repo.owner.username, owner: repo.owner.username,
@@ -291,8 +296,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.username} {repo.owner.username}
</span> </span>
@@ -300,7 +305,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
repo.name === field.value.repo repo.name === field.value.repo &&
repo.owner.username === field.value.owner
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
@@ -348,7 +354,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branches..." placeholder="Search branches..."
@@ -365,8 +374,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name) form.setValue("branch", branch.name)
} }
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
{branch.name} <span className="truncate">
{branch.name}
</span>
</span> </span>
<CheckIcon <CheckIcon
className={cn( className={cn(

View File

@@ -245,14 +245,19 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
: isLoadingRepositories : isLoadingRepositories
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) =>
repo.name === field.value.repo &&
repo.owner.login === field.value.owner,
)?.name ?? field.value.repo)} )?.name ?? field.value.repo)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -272,7 +277,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CommandGroup> <CommandGroup>
{repositories?.map((repo) => ( {repositories?.map((repo) => (
<CommandItem <CommandItem
value={repo.name} value={`${repo.owner.login}/${repo.name}`}
key={repo.url} key={repo.url}
onSelect={() => { onSelect={() => {
form.setValue("repository", { form.setValue("repository", {
@@ -282,8 +287,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.login} {repo.owner.login}
</span> </span>
@@ -291,7 +296,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
repo.name === field.value.repo repo.name === field.value.repo &&
repo.owner.login === field.value.owner
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
@@ -338,7 +344,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -366,7 +375,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -274,7 +274,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search repository..." placeholder="Search repository..."
@@ -312,8 +315,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
> >
<span className="flex items-center gap-2"> <span className="flex min-w-0 items-center gap-2">
<span>{repo.name}</span> <span className="truncate">
{repo.name}
</span>
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{repo.owner.username} {repo.owner.username}
</span> </span>
@@ -370,7 +375,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search branch..." placeholder="Search branch..."
@@ -398,7 +406,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name); form.setValue("branch", branch.name);
}} }}
> >
{branch.name} <span className="truncate">{branch.name}</span>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -97,23 +97,17 @@ export const getLogType = (message: string): LogStyle => {
return LOG_STYLES.info; return LOG_STYLES.info;
} }
// Key/value pairs that explicitly report a non-error (e.g. "error: none",
// "failed: false") must not trigger the error keyword patterns below
const nonErrorKeyValues =
/\b(?:error|err|errors|failed|failure|failures)s?\s*[:=]\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi;
const errorScope = lowerMessage.replace(nonErrorKeyValues, "");
if ( if (
/(?:^|\s)(?:error|err):?\s/i.test(errorScope) || /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
/\b(?:exception|failed|failure)\b/i.test(errorScope) || /\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
/(?:stack\s?trace):\s*$/i.test(errorScope) || /(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(errorScope) || /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(errorScope) || /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(errorScope) || /Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(errorScope) || /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
/\[(?:error|err|fatal)\]/i.test(errorScope) || /\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
/\b(?:crash|critical|fatal)\b/i.test(errorScope) || /\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(errorScope) /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
) { ) {
return LOG_STYLES.error; return LOG_STYLES.error;
} }

View File

@@ -227,9 +227,9 @@ export const HandleRegistry = ({ registryId }: Props) => {
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-2xl"> <DialogContent className="sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Add a external registry</DialogTitle> <DialogTitle>Add an external registry</DialogTitle>
<DialogDescription> <DialogDescription>
Fill the next fields to add a external registry. Fill in the following fields to add an external registry.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{(isError || testRegistryIsError || testRegistryByIdIsError) && ( {(isError || testRegistryIsError || testRegistryByIdIsError) && (

View File

@@ -1828,7 +1828,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className=""> <div className="">
<FormLabel>App Deploy</FormLabel> <FormLabel>App Deploy</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when a app is deployed. Trigger the action when an app is deployed.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -1890,7 +1890,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Dokploy Backup</FormLabel> <FormLabel>Dokploy Backup</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when a dokploy backup is created. Trigger the action when a Dokploy backup is created.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
@@ -1932,7 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Docker Cleanup</FormLabel> <FormLabel>Docker Cleanup</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when the docker cleanup is Trigger the action when Docker cleanup is
performed. performed.
</FormDescription> </FormDescription>
</div> </div>
@@ -1955,7 +1955,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel> <FormLabel>Dokploy Restart</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when dokploy is restarted. Trigger the action when Dokploy is restarted.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>

View File

@@ -43,7 +43,8 @@ const addServerDomain = z
.string() .string()
.trim() .trim()
.toLowerCase() .toLowerCase()
.refine((val) => VALID_HOSTNAME_REGEX.test(val), { // empty clears the server domain and reverts to IP-only access
.refine((val) => val === "" || VALID_HOSTNAME_REGEX.test(val), {
message: INVALID_HOSTNAME_MESSAGE, message: INVALID_HOSTNAME_MESSAGE,
}), }),
letsEncryptEmail: z.string(), letsEncryptEmail: z.string(),
@@ -51,7 +52,7 @@ const addServerDomain = z
certificateType: z.enum(["letsencrypt", "none", "custom"]), certificateType: z.enum(["letsencrypt", "none", "custom"]),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.https && !data.certificateType) { if (data.domain && data.https && !data.certificateType) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ["certificateType"], path: ["certificateType"],
@@ -59,6 +60,7 @@ const addServerDomain = z
}); });
} }
if ( if (
data.domain &&
data.https && data.https &&
data.certificateType === "letsencrypt" && data.certificateType === "letsencrypt" &&
!data.letsEncryptEmail !data.letsEncryptEmail

View File

@@ -151,13 +151,15 @@ function CommandItem({
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
className={cn( className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 in-data-[slot=dialog-content]:py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 in-data-[slot=dialog-content]:[&_svg:not([class*='size-'])]:size-5 data-selected:*:[svg]:text-foreground", "group/command-item relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 in-data-[slot=dialog-content]:py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 in-data-[slot=dialog-content]:[&_svg:not([class*='size-'])]:size-5 data-selected:*:[svg]:text-foreground",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" /> {"data-checked" in props && (
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
)}
</CommandPrimitive.Item> </CommandPrimitive.Item>
); );
} }

View File

@@ -80,7 +80,7 @@ function SelectContent({
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
data-position={position} data-position={position}
className={cn( className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", "p-1 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && "", position === "popper" && "",
)} )}
> >
@@ -114,7 +114,7 @@ function SelectItem({
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className, className,
)} )}
{...props} {...props}

View File

@@ -26,11 +26,14 @@ export const addSuffixToVolumesInServices = (
if (_.has(newServiceConfig, "volumes")) { if (_.has(newServiceConfig, "volumes")) {
newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => { newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => {
if (_.isString(volume)) { if (_.isString(volume)) {
const [volumeName, path] = volume.split(":"); // remainder is the container path plus optional access mode (:ro, :z, :Z)
const [volumeName, ...pathAndMode] = volume.split(":");
const remainder = pathAndMode.join(":");
// skip bind mounts and variables (e.g. $PWD) // skip bind mounts and variables (e.g. $PWD)
if ( if (
!volumeName || !volumeName ||
!remainder ||
volumeName.startsWith(".") || volumeName.startsWith(".") ||
volumeName.startsWith("/") || volumeName.startsWith("/") ||
volumeName.startsWith("$") volumeName.startsWith("$")
@@ -43,10 +46,10 @@ export const addSuffixToVolumesInServices = (
if (parts.length > 1) { if (parts.length > 1) {
const baseName = parts[0]; const baseName = parts[0];
const rest = parts.slice(1).join("/"); const rest = parts.slice(1).join("/");
return `${baseName}-${suffix}/${rest}:${path}`; return `${baseName}-${suffix}/${rest}:${remainder}`;
} }
return `${volumeName}-${suffix}:${path}`; return `${volumeName}-${suffix}:${remainder}`;
} }
if (_.isObject(volume) && volume.type === "volume" && volume.source) { if (_.isObject(volume) && volume.type === "volume" && volume.source) {
return { return {