mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-06 06:25:28 +02:00
Compare commits
33 Commits
v0.24.7
...
feat/label
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09b2492585 | ||
|
|
ca1fa7c4f7 | ||
|
|
112b898d98 | ||
|
|
15e62961e8 | ||
|
|
429c1e4cd8 | ||
|
|
1904a3d1e9 | ||
|
|
9763dce045 | ||
|
|
ef6dcaf363 | ||
|
|
37b056cd4b | ||
|
|
8bbef02e39 | ||
|
|
231b8ed19d | ||
|
|
cfa0135932 | ||
|
|
85bce827eb | ||
|
|
1fe12ba93e | ||
|
|
4b1146ab6b | ||
|
|
fe2f6f842b | ||
|
|
c3f29c2694 | ||
|
|
d5307cb5d6 | ||
|
|
b99556b389 | ||
|
|
112a1dedec | ||
|
|
edbdc01a1e | ||
|
|
883e1d1bfe | ||
|
|
33d6c2073b | ||
|
|
33873ce1e9 | ||
|
|
1d94c85c2b | ||
|
|
1758655f66 | ||
|
|
22c7c6e6fb | ||
|
|
025d439f71 | ||
|
|
9baafb83ff | ||
|
|
1f9ef473f1 | ||
|
|
a0bbf7be23 | ||
|
|
a5bc384d77 | ||
|
|
f2ae39aa86 |
39
.github/workflows/pull-request.yml
vendored
39
.github/workflows/pull-request.yml
vendored
@@ -4,9 +4,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, canary]
|
branches: [main, canary]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-and-typecheck:
|
pr-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
job: [build, test, typecheck]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
@@ -15,32 +21,5 @@ jobs:
|
|||||||
node-version: 20.16.0
|
node-version: 20.16.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm server:build
|
||||||
- run: pnpm typecheck
|
- run: pnpm ${{ matrix.job }}
|
||||||
|
|
||||||
build-and-test:
|
|
||||||
needs: lint-and-typecheck
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.16.0
|
|
||||||
cache: "pnpm"
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm build
|
|
||||||
|
|
||||||
parallel-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.16.0
|
|
||||||
cache: "pnpm"
|
|
||||||
- run: pnpm install --frozen-lockfile
|
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm test
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -27,9 +28,15 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Settings2 } from "lucide-react";
|
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -42,6 +49,7 @@ const schema = z
|
|||||||
wildcardDomain: z.string(),
|
wildcardDomain: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
previewLimit: z.number(),
|
previewLimit: z.number(),
|
||||||
|
previewLabels: z.array(z.string()).optional(),
|
||||||
previewHttps: z.boolean(),
|
previewHttps: z.boolean(),
|
||||||
previewPath: z.string(),
|
previewPath: z.string(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
@@ -81,6 +89,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
wildcardDomain: "*.traefik.me",
|
wildcardDomain: "*.traefik.me",
|
||||||
port: 3000,
|
port: 3000,
|
||||||
previewLimit: 3,
|
previewLimit: 3,
|
||||||
|
previewLabels: [],
|
||||||
previewHttps: false,
|
previewHttps: false,
|
||||||
previewPath: "/",
|
previewPath: "/",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
@@ -102,6 +111,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
buildArgs: data.previewBuildArgs || "",
|
buildArgs: data.previewBuildArgs || "",
|
||||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
port: data.previewPort || 3000,
|
port: data.previewPort || 3000,
|
||||||
|
previewLabels: data.previewLabels || [],
|
||||||
previewLimit: data.previewLimit || 3,
|
previewLimit: data.previewLimit || 3,
|
||||||
previewHttps: data.previewHttps || false,
|
previewHttps: data.previewHttps || false,
|
||||||
previewPath: data.previewPath || "/",
|
previewPath: data.previewPath || "/",
|
||||||
@@ -119,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewBuildArgs: formData.buildArgs,
|
previewBuildArgs: formData.buildArgs,
|
||||||
previewWildcard: formData.wildcardDomain,
|
previewWildcard: formData.wildcardDomain,
|
||||||
previewPort: formData.port,
|
previewPort: formData.port,
|
||||||
|
previewLabels: formData.previewLabels,
|
||||||
applicationId,
|
applicationId,
|
||||||
previewLimit: formData.previewLimit,
|
previewLimit: formData.previewLimit,
|
||||||
previewHttps: formData.previewHttps,
|
previewHttps: formData.previewHttps,
|
||||||
@@ -200,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="previewLabels"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Preview Labels</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add a labels that will trigger a preview
|
||||||
|
deployment for a pull request. If no labels
|
||||||
|
are specified, all pull requests will trigger
|
||||||
|
a preview deployment.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((label, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newLabels = [...(field.value || [])];
|
||||||
|
newLabels.splice(index, 1);
|
||||||
|
field.onChange(newLabels);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a label (e.g. enhancements, needs-review)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const label = input.value.trim();
|
||||||
|
if (label) {
|
||||||
|
field.onChange([
|
||||||
|
...(field.value || []),
|
||||||
|
label,
|
||||||
|
]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a label"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const label = input.value.trim();
|
||||||
|
if (label) {
|
||||||
|
field.onChange([...(field.value || []), label]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="previewLimit"
|
name="previewLimit"
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
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 {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema for Isolated Deployment
|
||||||
|
const isolatedSchema = z.object({
|
||||||
|
isolatedDeployment: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IsolatedSchema = z.infer<typeof isolatedSchema>;
|
||||||
|
|
||||||
|
export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [compose, setCompose] = useState<string>("");
|
||||||
|
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
|
||||||
|
const { mutateAsync, error, isError } =
|
||||||
|
api.compose.isolatedDeployment.useMutation();
|
||||||
|
|
||||||
|
const [isOpenPreview, setIsOpenPreview] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<IsolatedSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
isolatedDeployment: false,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(isolatedSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
isolatedDeployment: data?.isolatedDeployment || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: IsolatedSchema) => {
|
||||||
|
await updateCompose({
|
||||||
|
composeId,
|
||||||
|
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||||
|
})
|
||||||
|
.then(async (_data) => {
|
||||||
|
await refetch();
|
||||||
|
toast.success("Compose updated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePreview = async () => {
|
||||||
|
setIsOpenPreview(true);
|
||||||
|
setIsPreviewLoading(true);
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
suffix: data?.appName || "",
|
||||||
|
}).then(async (data) => {
|
||||||
|
await utils.project.all.invalidate();
|
||||||
|
setCompose(data);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
toast.error("Error generating preview");
|
||||||
|
setIsOpenPreview(false);
|
||||||
|
} finally {
|
||||||
|
setIsPreviewLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Enable Isolated Deployment</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure isolated deployment to the compose file.
|
||||||
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
|
<span>
|
||||||
|
This feature creates an isolated environment for your deployment
|
||||||
|
by adding unique prefixes to all resources. It establishes a
|
||||||
|
dedicated network based on your compose file's name, ensuring your
|
||||||
|
services run in isolation. This prevents conflicts when running
|
||||||
|
multiple instances of the same template or services with identical
|
||||||
|
names.
|
||||||
|
</span>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">
|
||||||
|
Resources that will be isolated:
|
||||||
|
</h4>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
<li>Docker networks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="isolated-deployment-form"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
{isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-col gap-4 w-full">
|
||||||
|
<div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isolatedDeployment"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
Enable Isolated Deployment ({data?.appName})
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable isolated deployment to the compose file.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
form="isolated-deployment-form"
|
||||||
|
type="submit"
|
||||||
|
className="lg:w-fit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={generatePreview}
|
||||||
|
isLoading={isPreviewLoading}
|
||||||
|
variant="secondary"
|
||||||
|
className="lg:w-fit"
|
||||||
|
>
|
||||||
|
Preview Compose
|
||||||
|
</Button>
|
||||||
|
<Dialog open={isOpenPreview} onOpenChange={setIsOpenPreview}>
|
||||||
|
<DialogContent className="sm:max-w-6xl max-h-[80vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Isolated Deployment Preview</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Preview of the compose file with isolated deployment
|
||||||
|
configuration
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 overflow-auto">
|
||||||
|
{isPreviewLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Generating compose preview...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="60vh"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -8,13 +13,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
import { ShowUtilities } from "./show-utilities";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
@@ -142,9 +141,7 @@ services:
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end"></div>
|
||||||
<ShowUtilities composeId={composeId} />
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="hook-form-save-compose-file"
|
form="hook-form-save-compose-file"
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
isolatedDeployment: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
export const IsolatedDeployment = ({ composeId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [compose, setCompose] = useState<string>("");
|
|
||||||
const { mutateAsync, error, isError } =
|
|
||||||
api.compose.isolatedDeployment.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
|
||||||
{ composeId },
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
|
||||||
defaultValues: {
|
|
||||||
isolatedDeployment: false,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
randomizeCompose();
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
isolatedDeployment: data?.isolatedDeployment || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: Schema) => {
|
|
||||||
await updateCompose({
|
|
||||||
composeId,
|
|
||||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
|
||||||
})
|
|
||||||
.then(async (_data) => {
|
|
||||||
await randomizeCompose();
|
|
||||||
await refetch();
|
|
||||||
toast.success("Compose updated");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the compose");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const randomizeCompose = async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
suffix: data?.appName || "",
|
|
||||||
}).then(async (data) => {
|
|
||||||
await utils.project.all.invalidate();
|
|
||||||
setCompose(data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Isolate Deployment</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Use this option to isolate the deployment of this compose file.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
|
||||||
<span>
|
|
||||||
This feature creates an isolated environment for your deployment by
|
|
||||||
adding unique prefixes to all resources. It establishes a dedicated
|
|
||||||
network based on your compose file's name, ensuring your services run
|
|
||||||
in isolation. This prevents conflicts when running multiple instances
|
|
||||||
of the same template or services with identical names.
|
|
||||||
</span>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">
|
|
||||||
Resources that will be isolated:
|
|
||||||
</h4>
|
|
||||||
<ul className="list-disc list-inside">
|
|
||||||
<li>Docker volumes</li>
|
|
||||||
<li>Docker networks</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-add-project"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="isolatedDeployment"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>
|
|
||||||
Enable Isolated Deployment ({data?.appName})
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Enable isolated deployment to the compose file.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
|
||||||
<Button
|
|
||||||
form="hook-form-add-project"
|
|
||||||
type="submit"
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Label>Preview</Label>
|
|
||||||
<pre>
|
|
||||||
<CodeEditor
|
|
||||||
value={compose || ""}
|
|
||||||
language="yaml"
|
|
||||||
readOnly
|
|
||||||
height="50rem"
|
|
||||||
/>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -62,7 +62,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="info" className="mb-4">
|
||||||
Preview your docker-compose file with added domains. Note: At least
|
Preview your docker-compose file with added domains. Note: At least
|
||||||
one domain must be specified for this conversion to take effect.
|
one domain must be specified for this conversion to take effect.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { IsolatedDeployment } from "./isolated-deployment";
|
|
||||||
import { RandomizeCompose } from "./randomize-compose";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowUtilities = ({ composeId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost">Show Utilities</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-5xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Utilities </DialogTitle>
|
|
||||||
<DialogDescription>Modify the application data</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Tabs defaultValue="isolated">
|
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="isolated">Isolated Deployment</TabsTrigger>
|
|
||||||
<TabsTrigger value="randomize">Randomize Compose</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="randomize" className="pt-5">
|
|
||||||
<RandomizeCompose composeId={composeId} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="isolated" className="pt-5">
|
|
||||||
<IsolatedDeployment composeId={composeId} />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
1
apps/dokploy/drizzle/0106_purple_maggott.sql
Normal file
1
apps/dokploy/drizzle/0106_purple_maggott.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "previewLabels" text[];
|
||||||
6424
apps/dokploy/drizzle/meta/0106_snapshot.json
Normal file
6424
apps/dokploy/drizzle/meta/0106_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -743,6 +743,13 @@
|
|||||||
"when": 1754259281559,
|
"when": 1754259281559,
|
||||||
"tag": "0105_clumsy_quicksilver",
|
"tag": "0105_clumsy_quicksilver",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 106,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754912062243,
|
||||||
|
"tag": "0106_purple_maggott",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
77
apps/dokploy/hooks/use-keyboard-nav.tsx
Normal file
77
apps/dokploy/hooks/use-keyboard-nav.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const SHORTCUTS = {
|
||||||
|
g: "general",
|
||||||
|
e: "environment",
|
||||||
|
u: "domains",
|
||||||
|
p: "preview-deployments",
|
||||||
|
s: "schedules",
|
||||||
|
v: "volume-backups",
|
||||||
|
d: "deployments",
|
||||||
|
l: "logs",
|
||||||
|
m: "monitoring",
|
||||||
|
a: "advanced",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to register keyboard shortcuts for the application page. Each
|
||||||
|
* shortcut must be prefixed with `g` (like GitHub).
|
||||||
|
*
|
||||||
|
* - `g g` "General",
|
||||||
|
* - `g e` "Environment",
|
||||||
|
* - `g u` "Domains",
|
||||||
|
* - `g p` "Preview Deployments",
|
||||||
|
* - `g s` "Schedules",
|
||||||
|
* - `g v` "Volume Backups",
|
||||||
|
* - `g d` "Deployments",
|
||||||
|
* - `g l` "Logs",
|
||||||
|
* - `g m` "Monitoring",
|
||||||
|
* - `g a` "Advanced"
|
||||||
|
*/
|
||||||
|
export function UseKeyboardNavForApplications() {
|
||||||
|
const [isModPressed, setModPressed] = useState(false);
|
||||||
|
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const sp = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const updateSearchParam = useCallback(
|
||||||
|
(name: string, value: string) => {
|
||||||
|
const params = new URLSearchParams(sp.toString());
|
||||||
|
params.set(name, value);
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
},
|
||||||
|
[sp],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = ({ key }: KeyboardEvent) => {
|
||||||
|
if (isModPressed) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
setModPressed(false);
|
||||||
|
|
||||||
|
if (key in SHORTCUTS) {
|
||||||
|
const tab = SHORTCUTS[key as keyof typeof SHORTCUTS];
|
||||||
|
router.push(
|
||||||
|
`${pathname}?${updateSearchParam("tab", tab.toLowerCase())}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (key === "g") {
|
||||||
|
setModPressed(true);
|
||||||
|
setTimer(setTimeout(() => setModPressed(false), 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isModPressed, timer, updateSearchParam, router, pathname]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.24.7",
|
"version": "v0.24.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { applications, compose, github } from "@/server/db/schema";
|
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { deploy } from "@/server/utils/deploy";
|
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
|
||||||
checkUserRepositoryPermissions,
|
checkUserRepositoryPermissions,
|
||||||
createPreviewDeployment,
|
createPreviewDeployment,
|
||||||
createSecurityBlockedComment,
|
createSecurityBlockedComment,
|
||||||
findGithubById,
|
findGithubById,
|
||||||
findPreviewDeploymentByApplicationId,
|
findPreviewDeploymentByApplicationId,
|
||||||
findPreviewDeploymentsByPullRequestId,
|
findPreviewDeploymentsByPullRequestId,
|
||||||
|
IS_CLOUD,
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
shouldDeploy,
|
shouldDeploy,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { applications, compose, github } from "@/server/db/schema";
|
||||||
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
@@ -343,7 +343,9 @@ export default async function handler(
|
|||||||
if (
|
if (
|
||||||
action === "opened" ||
|
action === "opened" ||
|
||||||
action === "synchronize" ||
|
action === "synchronize" ||
|
||||||
action === "reopened"
|
action === "reopened" ||
|
||||||
|
action === "labeled" ||
|
||||||
|
action === "unlabeled"
|
||||||
) {
|
) {
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||||
@@ -442,6 +444,19 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const app of secureApps) {
|
for (const app of secureApps) {
|
||||||
|
// check for labels
|
||||||
|
if (app?.previewLabels && app?.previewLabels?.length > 0) {
|
||||||
|
let hasLabel = false;
|
||||||
|
const labels = githubBody?.pull_request?.labels;
|
||||||
|
for (const label of labels) {
|
||||||
|
if (app?.previewLabels?.includes(label.name)) {
|
||||||
|
hasLabel = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasLabel) continue;
|
||||||
|
}
|
||||||
|
|
||||||
const previewLimit = app?.previewLimit || 0;
|
const previewLimit = app?.previewLimit || 0;
|
||||||
if (app?.previewDeployments?.length > previewLimit) {
|
if (app?.previewDeployments?.length > previewLimit) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { UseKeyboardNavForApplications } from "@/hooks/use-keyboard-nav";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ const Service = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
|
<UseKeyboardNavForApplications />
|
||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[
|
list={[
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
{ name: "Projects", href: "/dashboard/projects" },
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { CircuitBoard, HelpCircle, ServerOff } from "lucide-react";
|
||||||
|
import type {
|
||||||
|
GetServerSidePropsContext,
|
||||||
|
InferGetServerSidePropsType,
|
||||||
|
} from "next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import superjson from "superjson";
|
||||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
||||||
@@ -6,6 +20,7 @@ import { ShowEnvironment } from "@/components/dashboard/application/environment/
|
|||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
||||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||||
|
import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||||
@@ -35,21 +50,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import type {
|
|
||||||
GetServerSidePropsContext,
|
|
||||||
InferGetServerSidePropsType,
|
|
||||||
} from "next";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { type ReactElement, useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import superjson from "superjson";
|
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -351,6 +351,7 @@ const Service = (
|
|||||||
<AddCommandCompose composeId={composeId} />
|
<AddCommandCompose composeId={composeId} />
|
||||||
<ShowVolumes id={composeId} type="compose" />
|
<ShowVolumes id={composeId} type="compose" />
|
||||||
<ShowImport composeId={composeId} />
|
<ShowImport composeId={composeId} />
|
||||||
|
<IsolatedDeploymentTab composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -53,14 +53,21 @@ export const swarmRouter = createTRPCRouter({
|
|||||||
return getNodeApplications(input.serverId);
|
return getNodeApplications(input.serverId);
|
||||||
}),
|
}),
|
||||||
getAppInfos: protectedProcedure
|
getAppInfos: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
path: "/drop-deployment",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z
|
appName: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.regex(containerIdRegex, "Invalid app name.")
|
.regex(containerIdRegex, "Invalid app name.")
|
||||||
.array()
|
.array(),
|
||||||
.min(1),
|
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const applications = pgTable("application", {
|
|||||||
previewEnv: text("previewEnv"),
|
previewEnv: text("previewEnv"),
|
||||||
watchPaths: text("watchPaths").array(),
|
watchPaths: text("watchPaths").array(),
|
||||||
previewBuildArgs: text("previewBuildArgs"),
|
previewBuildArgs: text("previewBuildArgs"),
|
||||||
|
previewLabels: text("previewLabels").array(),
|
||||||
previewWildcard: text("previewWildcard"),
|
previewWildcard: text("previewWildcard"),
|
||||||
previewPort: integer("previewPort").default(3000),
|
previewPort: integer("previewPort").default(3000),
|
||||||
previewHttps: boolean("previewHttps").notNull().default(false),
|
previewHttps: boolean("previewHttps").notNull().default(false),
|
||||||
@@ -308,6 +309,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
previewLabels: z.array(z.string()).optional(),
|
||||||
cleanCache: z.boolean().optional(),
|
cleanCache: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -391,22 +391,22 @@ export const readPorts = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeTraefikSetup = async (
|
export const writeTraefikSetup = async (input: TraefikOptions) => {
|
||||||
input: TraefikOptions,
|
const resourceType = await getDockerResourceType(
|
||||||
serverId?: string,
|
"dokploy-traefik",
|
||||||
) => {
|
input.serverId,
|
||||||
const resourceType = await getDockerResourceType("dokploy-traefik", serverId);
|
);
|
||||||
if (resourceType === "service") {
|
if (resourceType === "service") {
|
||||||
await initializeTraefikService({
|
await initializeTraefikService({
|
||||||
env: input.env,
|
env: input.env,
|
||||||
additionalPorts: input.additionalPorts,
|
additionalPorts: input.additionalPorts,
|
||||||
serverId: serverId,
|
serverId: input.serverId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await initializeStandaloneTraefik({
|
await initializeStandaloneTraefik({
|
||||||
env: input.env,
|
env: input.env,
|
||||||
additionalPorts: input.additionalPorts,
|
additionalPorts: input.additionalPorts,
|
||||||
serverId: serverId,
|
serverId: input.serverId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,21 +89,14 @@ export const initializeStandaloneTraefik = async ({
|
|||||||
const docker = await getRemoteDocker(serverId);
|
const docker = await getRemoteDocker(serverId);
|
||||||
try {
|
try {
|
||||||
const container = docker.getContainer(containerName);
|
const container = docker.getContainer(containerName);
|
||||||
try {
|
await container.remove({ force: true });
|
||||||
await container.remove({ force: true });
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
} catch {}
|
||||||
await docker.createContainer(settings);
|
|
||||||
const newContainer = docker.getContainer(containerName);
|
await docker.createContainer(settings);
|
||||||
await newContainer.start();
|
const newContainer = docker.getContainer(containerName);
|
||||||
console.log("Traefik Started ✅");
|
await newContainer.start();
|
||||||
} catch (error) {
|
console.log("Traefik Started ✅");
|
||||||
console.error("Error in initializeStandaloneTraefik", error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await docker.createContainer(settings);
|
|
||||||
console.error("Error in initializeStandaloneTraefik", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initializeTraefikService = async ({
|
export const initializeTraefikService = async ({
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { findComposeById } from "@dokploy/server/services/compose";
|
import { findComposeById } from "@dokploy/server/services/compose";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
||||||
import { generateRandomHash } from "./compose";
|
import { generateRandomHash } from "./compose";
|
||||||
import { addSuffixToAllVolumes } from "./compose/volume";
|
import { addSuffixToAllVolumes } from "./compose/volume";
|
||||||
|
import {
|
||||||
|
cloneCompose,
|
||||||
|
cloneComposeRemote,
|
||||||
|
loadDockerCompose,
|
||||||
|
loadDockerComposeRemote,
|
||||||
|
} from "./domain";
|
||||||
import type { ComposeSpecification } from "./types";
|
import type { ComposeSpecification } from "./types";
|
||||||
|
|
||||||
export const addAppNameToPreventCollision = (
|
export const addAppNameToPreventCollision = (
|
||||||
@@ -24,16 +30,34 @@ export const randomizeIsolatedDeploymentComposeFile = async (
|
|||||||
suffix?: string,
|
suffix?: string,
|
||||||
) => {
|
) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const composeFile = compose.composeFile;
|
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
if (compose.serverId) {
|
||||||
|
await cloneComposeRemote(compose);
|
||||||
|
} else {
|
||||||
|
await cloneCompose(compose);
|
||||||
|
}
|
||||||
|
|
||||||
|
let composeData: ComposeSpecification | null;
|
||||||
|
|
||||||
|
if (compose.serverId) {
|
||||||
|
composeData = await loadDockerComposeRemote(compose);
|
||||||
|
} else {
|
||||||
|
composeData = await loadDockerCompose(compose);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!composeData) {
|
||||||
|
throw new Error("Compose data not found");
|
||||||
|
}
|
||||||
|
|
||||||
const randomSuffix = suffix || compose.appName || generateRandomHash();
|
const randomSuffix = suffix || compose.appName || generateRandomHash();
|
||||||
|
|
||||||
const newComposeFile = addAppNameToPreventCollision(
|
const newComposeFile = compose.isolatedDeployment
|
||||||
composeData,
|
? addAppNameToPreventCollision(
|
||||||
randomSuffix,
|
composeData,
|
||||||
compose.isolatedDeploymentsVolume,
|
randomSuffix,
|
||||||
);
|
compose.isolatedDeploymentsVolume,
|
||||||
|
)
|
||||||
|
: composeData;
|
||||||
|
|
||||||
return dump(newComposeFile);
|
return dump(newComposeFile);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export const getGitlabBranches = async (input: {
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const branchesResponse = await fetch(
|
const branchesResponse = await fetch(
|
||||||
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
|
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user