Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/dokploy/__test__/drop/drop.test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const baseApp: ApplicationNested = {
enableSubmodules: false,
applicationStatus: "done",
triggerType: "push",
deployRegistryId: null,
deployImage: null,
deployImageTag: "latest",
deployRegistry: null,
appName: "",
autoDeploy: true,
serverId: "",
Expand Down
4 changes: 4 additions & 0 deletions apps/dokploy/__test__/traefik/traefik.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const baseApp: ApplicationNested = {
isPreviewDeploymentsActive: false,
previewBuildArgs: null,
triggerType: "push",
deployRegistryId: null,
deployImage: null,
deployImageTag: "latest",
deployRegistry: null,
previewCertificateType: "none",
previewEnv: null,
previewHttps: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

const RegistryProviderSchema = z.object({
deployRegistryId: z.string().min(1, {
message: "Registry is required",
}),
deployImage: z.string().min(1, {
message: "Image is required",
}),
deployImageTag: z.string().min(1, {
message: "Tag is required",
}),
});

type RegistryProvider = z.infer<typeof RegistryProviderSchema>;

interface Props {
applicationId: string;
}

export const SaveRegistryProvider = ({ applicationId }: Props) => {
const [selectedRegistryId, setSelectedRegistryId] = useState<string>("");
const [selectedImage, setSelectedImage] = useState<string>("");

const { data: application, refetch } = api.application.one.useQuery({
applicationId,
});
const { data: registries } = api.application.getUserRegistries.useQuery();

const { data: images, isLoading: imagesLoading } =
api.application.getRegistryImages.useQuery(
{ registryId: selectedRegistryId },
{ enabled: !!selectedRegistryId },
);

const { data: tags, isLoading: tagsLoading } =
api.application.getImageTags.useQuery(
{ registryId: selectedRegistryId, imageName: selectedImage },
{ enabled: !!selectedRegistryId && !!selectedImage },
);

const { mutateAsync } = api.application.saveRegistryProvider.useMutation();

const form = useForm<RegistryProvider>({
defaultValues: {
deployRegistryId: "",
deployImage: "",
deployImageTag: "latest",
},
resolver: zodResolver(RegistryProviderSchema),
});

useEffect(() => {
if (application) {
const values = {
deployRegistryId: application.deployRegistryId || "",
deployImage: application.deployImage || "",
deployImageTag: application.deployImageTag || "latest",
};
form.reset(values);
setSelectedRegistryId(values.deployRegistryId);
setSelectedImage(values.deployImage);
}
}, [form, application]);

const onSubmit = async (values: RegistryProvider) => {
await mutateAsync({
applicationId,
...values,
})
.then(async () => {
toast.success("Registry Provider Saved");
await refetch();
})
.catch(() => {
toast.error("Error saving the Registry provider");
});
};

return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="deployRegistryId"
render={({ field }) => (
<FormItem>
<FormLabel>Registry</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
setSelectedRegistryId(value);
setSelectedImage("");
form.setValue("deployImage", "");
form.setValue("deployImageTag", "latest");
}}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a registry" />
</SelectTrigger>
</FormControl>
<SelectContent>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="deployImage"
render={({ field }) => (
<FormItem>
<FormLabel>Image</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
setSelectedImage(value);
form.setValue("deployImageTag", "latest");
}}
value={field.value}
disabled={!selectedRegistryId}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={
imagesLoading
? "Loading images..."
: "Select an image"
}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{images?.map((image: string) => (
<SelectItem key={image} value={image}>
{image}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid md:grid-cols-1 gap-4">
<FormField
control={form.control}
name="deployImageTag"
render={({ field }) => (
<FormItem>
<div className="flex flex-row gap-2">
<FormLabel>Tag</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
<p>
If <code>Daily Docker Cleanup</code> is enabled or if
there are any factors that regularly delete tags, it
is recommended to use the <code>'latest'</code> tag.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select
onValueChange={field.onChange}
value={field.value}
disabled={!selectedImage}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={
tagsLoading ? "Loading tags..." : "Select a tag"
}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{tags?.map((tag: string) => (
<SelectItem key={tag} value={tag}>
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>

<div className="flex flex-row justify-end">
<Button
type="submit"
className="w-fit"
isLoading={form.formState.isSubmitting}
>
Save
</Button>
</div>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SaveDockerProvider } from "@/components/dashboard/application/general/g
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
import { SaveRegistryProvider } from "@/components/dashboard/application/general/generic/save-registry-provider";
import {
BitbucketIcon,
DockerIcon,
Expand Down Expand Up @@ -29,7 +30,8 @@ type TabState =
| "drop"
| "gitlab"
| "bitbucket"
| "gitea";
| "gitea"
| "registry";

interface Props {
applicationId: string;
Expand Down Expand Up @@ -103,7 +105,8 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
application &&
!application.hasGitProviderAccess &&
application.sourceType !== "docker" &&
application.sourceType !== "drop"
application.sourceType !== "drop" &&
application.sourceType !== "registry"
) {
return (
<Card className="group relative w-full bg-transparent">
Expand Down Expand Up @@ -190,6 +193,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
<DockerIcon className="size-5 text-current" />
Docker
</TabsTrigger>
<TabsTrigger
value="registry"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<DockerIcon className="size-5 text-current" />
Registry
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
Expand Down Expand Up @@ -291,6 +301,10 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
<SaveDockerProvider applicationId={applicationId} />
</TabsContent>

<TabsContent value="registry" className="w-full p-2">
<SaveRegistryProvider applicationId={applicationId} />
</TabsContent>

<TabsContent value="git" className="w-full p-2">
<SaveGitProvider applicationId={applicationId} />
</TabsContent>
Expand Down
5 changes: 5 additions & 0 deletions apps/dokploy/drizzle/0104_dashing_sharon_ventura.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TYPE "public"."sourceType" ADD VALUE 'registry';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "deployRegistryId" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "deployImage" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "deployImageTag" text DEFAULT 'latest';--> statement-breakpoint
ALTER TABLE "application" ADD CONSTRAINT "application_deployRegistryId_registry_registryId_fk" FOREIGN KEY ("deployRegistryId") REFERENCES "public"."registry"("registryId") ON DELETE set null ON UPDATE no action;
Loading