Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572579af91 | ||
|
|
63998f71ec | ||
|
|
45fd2d149c | ||
|
|
9a35c85277 | ||
|
|
0cf21cf3f7 | ||
|
|
7400913646 | ||
|
|
e78d354d0d | ||
|
|
bec3ad6bb5 | ||
|
|
5846e429e5 | ||
|
|
0f7652d02c | ||
|
|
fef19056fa | ||
|
|
b07d9939a6 | ||
|
|
301e3480e4 | ||
|
|
976ac053f7 | ||
|
|
f102bae5d5 | ||
|
|
00883dde11 | ||
|
|
e194f3c454 | ||
|
|
cdd39670f5 | ||
|
|
88f7cf2546 | ||
|
|
34ea7ad8c9 | ||
|
|
081a2d8f69 | ||
|
|
a6368ee0b8 | ||
|
|
4132f714ae | ||
|
|
333776a5a1 | ||
|
|
5853117e5f | ||
|
|
9e0e3540f5 | ||
|
|
7bd6e7fd9a | ||
|
|
95ab6af3ac | ||
|
|
69876029b1 | ||
|
|
d4fdf881cd | ||
|
|
3b14ebcaa4 | ||
|
|
22b8fa2c00 | ||
|
|
714865730f | ||
|
|
7469c30992 | ||
|
|
b296b6bbf0 | ||
|
|
37fa139a65 | ||
|
|
a1cf597c2b | ||
|
|
c8e9d9d169 | ||
|
|
d01928a878 | ||
|
|
30c19c5698 | ||
|
|
01dfa7feaf | ||
|
|
58e6462ff1 | ||
|
|
d18876d4fb | ||
|
|
492c912c61 | ||
|
|
6a283c8ee2 | ||
|
|
59dfdd6192 | ||
|
|
3c072d7aa8 | ||
|
|
19d897f3ad | ||
|
|
0477329db7 | ||
|
|
fabe946526 | ||
|
|
daa0c9d5d4 | ||
|
|
afe9b3c113 | ||
|
|
cbfd09786a | ||
|
|
54eb5544ac | ||
|
|
ac33b6b6a1 | ||
|
|
653b1972ca | ||
|
|
7d7eb6a7a2 | ||
|
|
fab7e138b7 | ||
|
|
62b635b2f0 | ||
|
|
2dd352ee76 | ||
|
|
422b6eea82 | ||
|
|
4850305fb6 | ||
|
|
97779f5686 | ||
|
|
d4b8985d71 | ||
|
|
d5686063e0 | ||
|
|
62ca8eec53 | ||
|
|
204143648d | ||
|
|
be8bd78bcc | ||
|
|
9003e43702 | ||
|
|
55ac24ee8e | ||
|
|
f3be56234b | ||
|
|
fd59beaff1 | ||
|
|
4a70d60aed | ||
|
|
f7533c88f6 | ||
|
|
ea8cae7815 | ||
|
|
e9956a66da | ||
|
|
2eeb4017ac | ||
|
|
28f0c9f162 | ||
|
|
4967d3bb31 | ||
|
|
238fa5d02d | ||
|
|
d1436c992e | ||
|
|
0654804821 | ||
|
|
c0876044b0 | ||
|
|
6dff11af22 | ||
|
|
6d674a4c6b | ||
|
|
96b2579d69 | ||
|
|
0708fa05b6 | ||
|
|
597842a99f | ||
|
|
105cf1014f | ||
|
|
b93f36ae77 | ||
|
|
bebb4b973c | ||
|
|
f790530d4d | ||
|
|
b7374549b8 | ||
|
|
366e881d72 | ||
|
|
c2125d82b1 | ||
|
|
32b3a76457 | ||
|
|
5cbdc8fad9 | ||
|
|
3698e8a827 | ||
|
|
a83b62f62b | ||
|
|
ac033cea22 | ||
|
|
58814239d9 | ||
|
|
6fc1ce2fbc | ||
|
|
adde8126ab | ||
|
|
cda66606ec | ||
|
|
28f2c1a3c0 | ||
|
|
1c5fe8a283 | ||
|
|
da005bc511 |
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm commitlint --edit $1
|
||||
@@ -1,6 +0,0 @@
|
||||
// Skip Husky install in production and CI
|
||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
||||
process.exit(0);
|
||||
}
|
||||
const husky = (await import("husky")).default;
|
||||
console.log(husky());
|
||||
@@ -1 +0,0 @@
|
||||
pnpm lint-staged
|
||||
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: ['bug']
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -11,18 +11,27 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
|
||||
description: |
|
||||
A detailed, step-by-step description of how to reproduce the issue is required.
|
||||
Please ensure your report includes clear instructions using numbered lists.
|
||||
|
||||
If possible, provide a link to a repository or project where the issue can be reproduced.
|
||||
placeholder: |
|
||||
1. Create a application
|
||||
2. Click X
|
||||
3. Y will happen
|
||||
|
||||
Make sure to:
|
||||
- Use numbered lists to outline steps clearly.
|
||||
- Include all relevant commands and configurations.
|
||||
- Provide a link to a reproducible repository if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current vs. Expected behavior
|
||||
description: A clear and concise description of what the bug is, and what you expected to happen.
|
||||
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
|
||||
placeholder: "Following the steps from the previous section, I expected A to happen, but I observed B instead"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -45,12 +54,23 @@ body:
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- 'Installation'
|
||||
- 'Application'
|
||||
- 'Databases'
|
||||
- 'Docker Compose'
|
||||
- 'Traefik'
|
||||
- 'Docker'
|
||||
- "Installation"
|
||||
- "Application"
|
||||
- "Databases"
|
||||
- "Docker Compose"
|
||||
- "Traefik"
|
||||
- "Docker"
|
||||
- "Remote server"
|
||||
- "Local Development"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Are you deploying the applications where Dokploy is installed or on a remote server?
|
||||
options:
|
||||
- "Same server where Dokploy is installed"
|
||||
- "Remote server"
|
||||
- "Both"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -59,4 +79,16 @@ body:
|
||||
description: |
|
||||
Any extra information that might help us investigate.
|
||||
placeholder: |
|
||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Will you send a PR to fix it?
|
||||
description: Let us know if you are planning to submit a pull request to address this issue.
|
||||
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
- "Maybe, need help"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement to the project
|
||||
labels: ['enhancement']
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -30,4 +30,15 @@ body:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Will you send a PR to implement it?
|
||||
description: Let us know if you are planning to submit a pull request to implement this feature.
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
- "Maybe, need help"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
BIN
.github/sponsors/startupfame.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
8
.github/workflows/pull-request.yml
vendored
@@ -4,9 +4,6 @@ on:
|
||||
pull_request:
|
||||
branches: [main, canary]
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
lint-and-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -18,8 +15,7 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm biome ci
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm typecheck
|
||||
|
||||
build-and-test:
|
||||
@@ -46,5 +42,5 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm test
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npx commitlint --edit "$1"
|
||||
@@ -1,6 +0,0 @@
|
||||
// Skip Husky install in production and CI
|
||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
||||
process.exit(0);
|
||||
}
|
||||
const husky = (await import("husky")).default;
|
||||
console.log(husky());
|
||||
@@ -1,2 +0,0 @@
|
||||
pnpm run check
|
||||
git add .
|
||||
@@ -87,7 +87,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Lightspeed.run"/></a>
|
||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||
</div>
|
||||
|
||||
### Community Backers 🤝
|
||||
@@ -116,7 +117,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
## Video Tutorial
|
||||
|
||||
<a href="https://youtu.be/mznYKPvhcfw">
|
||||
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
</a>
|
||||
|
||||
<!-- ## Supported OS
|
||||
|
||||
@@ -30,6 +30,7 @@ const baseApp: ApplicationNested = {
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
registryUrl: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
project: {
|
||||
|
||||
@@ -12,6 +12,7 @@ const baseApp: ApplicationNested = {
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
registryUrl: "",
|
||||
buildArgs: null,
|
||||
project: {
|
||||
env: "",
|
||||
|
||||
@@ -1,63 +1,143 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteApplicationSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Application name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const DeleteApplication = ({ applicationId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.application.delete.useMutation();
|
||||
const { data } = api.application.one.useQuery(
|
||||
{ applicationId },
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteApplication>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteApplicationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteApplication) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Application deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the application");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Project name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
|
||||
toast.success("Application delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete Application");
|
||||
});
|
||||
application. If you are sure please enter the application name to
|
||||
delete this application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-application"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter application name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-application"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -202,7 +202,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
@@ -281,7 +280,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -21,6 +21,7 @@ const DockerProviderSchema = z.object({
|
||||
}),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
registryURL: z.string().optional(),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
@@ -33,12 +34,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync } = api.application.saveDockerProvider.useMutation();
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {
|
||||
dockerImage: "",
|
||||
password: "",
|
||||
username: "",
|
||||
registryURL: "",
|
||||
},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
});
|
||||
@@ -49,6 +50,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
dockerImage: data.dockerImage || "",
|
||||
password: data.password || "",
|
||||
username: data.username || "",
|
||||
registryURL: data.registryUrl || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
@@ -59,6 +61,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
password: values.password || null,
|
||||
applicationId,
|
||||
username: values.username || null,
|
||||
registryUrl: values.registryURL || null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Docker Provider Saved");
|
||||
@@ -76,7 +79,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4 ">
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerImage"
|
||||
@@ -91,6 +94,19 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Registry URL" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -193,7 +193,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
@@ -272,7 +271,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -209,7 +209,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
@@ -297,7 +296,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -1,63 +1,141 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteComposeSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Compose name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const DeleteCompose = ({ composeId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteCompose>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteComposeSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteCompose) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ composeId })
|
||||
.then((result) => {
|
||||
push(`/dashboard/project/${result?.projectId}`);
|
||||
toast.success("Compose deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the compose");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: `Project name must match "${expectedName}"`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
compose and all its services.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
|
||||
toast.success("Compose delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the compose");
|
||||
});
|
||||
compose. If you are sure please enter the compose name to delete
|
||||
this compose.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-compose"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>{" "}
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter compose name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-compose"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -204,7 +204,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
@@ -283,7 +282,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -195,7 +195,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
@@ -274,7 +273,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -211,7 +211,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
@@ -299,7 +298,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -160,7 +160,6 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -144,7 +144,6 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
|
||||
@@ -1,62 +1,140 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteMariadbSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Database name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteMariadb = z.infer<typeof deleteMariadbSchema>;
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
}
|
||||
|
||||
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||
const { data } = api.mariadb.one.useQuery(
|
||||
{ mariadbId },
|
||||
{ enabled: !!mariadbId },
|
||||
);
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteMariadb>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteMariadbSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteMariadb) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ mariadbId })
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the database");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Database name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
database. If you are sure please enter the database name to delete
|
||||
this database.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-mariadb"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter database name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-mariadb"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,62 +1,139 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteMongoSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Database name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteMongo = z.infer<typeof deleteMongoSchema>;
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
}
|
||||
|
||||
// commen
|
||||
|
||||
export const DeleteMongo = ({ mongoId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||
const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId });
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteMongo>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteMongoSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteMongo) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ mongoId })
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the database");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Database name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
database. If you are sure please enter the database name to delete
|
||||
this database.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-mongo"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter database name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-mongo"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,62 +1,138 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteMysqlSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Database name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteMysql = z.infer<typeof deleteMysqlSchema>;
|
||||
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
}
|
||||
|
||||
export const DeleteMysql = ({ mysqlId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId });
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteMysql>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteMysqlSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteMysql) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ mysqlId })
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the database");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Database name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mysqlId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
database. If you are sure please enter the database name to delete
|
||||
this database.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-mysql"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter database name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-mysql"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -86,14 +86,12 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
|
||||
)}
|
||||
|
||||
{mount.type === "file" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{mount.type === "bind" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -1,62 +1,141 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deletePostgresSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Database name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeletePostgres = z.infer<typeof deletePostgresSchema>;
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
}
|
||||
|
||||
export const DeletePostgres = ({ postgresId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
||||
const { data } = api.postgres.one.useQuery(
|
||||
{ postgresId },
|
||||
{ enabled: !!postgresId },
|
||||
);
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeletePostgres>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deletePostgresSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeletePostgres) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ postgresId })
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the database");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Database name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
postgresId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
database. If you are sure please enter the database name to delete
|
||||
this database.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-postgres"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter database name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-postgres"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -127,7 +127,6 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"md:max-w-[15rem] w-full justify-between !bg-input",
|
||||
)}
|
||||
|
||||
@@ -19,11 +19,9 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, FileIcon, SquarePen } from "lucide-react";
|
||||
import { FileIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -37,9 +35,10 @@ type UpdateProject = z.infer<typeof updateProjectSchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddEnv = ({ projectId }: Props) => {
|
||||
export const ProjectEnviroment = ({ projectId, children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
@@ -53,7 +52,6 @@ export const AddEnv = ({ projectId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
console.log(data);
|
||||
const form = useForm<UpdateProject>({
|
||||
defaultValues: {
|
||||
env: data?.env ?? "",
|
||||
@@ -86,34 +84,29 @@ export const AddEnv = ({ projectId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<FileIcon className="size-4" />
|
||||
<span>Add Env</span>
|
||||
</DropdownMenuItem>
|
||||
{children ?? (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<FileIcon className="size-4" />
|
||||
<span>Project Enviroment</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modify Shared Env</DialogTitle>
|
||||
<DialogDescription>Update the env variables</DialogDescription>
|
||||
<DialogTitle>Project Enviroment</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the env Environment variables that are accessible to all
|
||||
services of this project. Use this syntax to reference project-level
|
||||
variables in your service environments:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<AlertBlock type="info">
|
||||
To use a shared env, in one of your services, you need to use like
|
||||
this: Let's say you have a shared env ENVIROMENT="development" and you
|
||||
want to use it in your service, you need to use like this:
|
||||
<ul>
|
||||
<li>
|
||||
<code>ENVIRONMENT=${"{{project.ENVIRONMENT}}"}</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
|
||||
</li>
|
||||
</ul>{" "}
|
||||
This allows the service to inherit and use the shared variables from
|
||||
the project level, ensuring consistency across services.
|
||||
Use this syntax to reference project-level variables in your service
|
||||
environments: <code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
|
||||
</AlertBlock>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
@@ -25,7 +25,6 @@ import { api } from "@/utils/api";
|
||||
import {
|
||||
AlertTriangle,
|
||||
BookIcon,
|
||||
CircuitBoard,
|
||||
ExternalLink,
|
||||
ExternalLinkIcon,
|
||||
FolderInput,
|
||||
@@ -35,7 +34,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AddEnv } from "./add-env";
|
||||
import { ProjectEnviroment } from "./project-enviroment";
|
||||
import { UpdateProject } from "./update";
|
||||
|
||||
export const ShowProjects = () => {
|
||||
@@ -192,7 +191,9 @@ export const ShowProjects = () => {
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<AddEnv projectId={project.projectId} />
|
||||
<ProjectEnviroment
|
||||
projectId={project.projectId}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<UpdateProject projectId={project.projectId} />
|
||||
|
||||
@@ -1,62 +1,138 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const deleteRedisSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Database name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type DeleteRedis = z.infer<typeof deleteRedisSchema>;
|
||||
|
||||
interface Props {
|
||||
redisId: string;
|
||||
}
|
||||
|
||||
export const DeleteRedis = ({ redisId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
||||
const { data } = api.redis.one.useQuery({ redisId }, { enabled: !!redisId });
|
||||
const { push } = useRouter();
|
||||
const form = useForm<DeleteRedis>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
},
|
||||
resolver: zodResolver(deleteRedisSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: DeleteRedis) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ redisId })
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database deleted successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting the database");
|
||||
});
|
||||
} else {
|
||||
form.setError("projectName", {
|
||||
message: "Database name does not match",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
redisId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
database. If you are sure please enter the database name to delete
|
||||
this database.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-redis"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter database name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-redis"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,9 +37,12 @@ const appearanceFormSchema = z.object({
|
||||
theme: z.enum(["light", "dark", "system"], {
|
||||
required_error: "Please select a theme.",
|
||||
}),
|
||||
language: z.enum(["en", "pl", "zh-Hans"], {
|
||||
required_error: "Please select a language.",
|
||||
}),
|
||||
language: z.enum(
|
||||
["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
|
||||
{
|
||||
required_error: "Please select a language.",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
||||
@@ -175,7 +178,16 @@ export function AppearanceForm() {
|
||||
{[
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Polski", value: "pl" },
|
||||
{ label: "Русский", value: "ru" },
|
||||
{ label: "Français", value: "fr" },
|
||||
{ label: "Deutsch", value: "de" },
|
||||
{ label: "繁體中文", value: "zh-Hant" },
|
||||
{ label: "简体中文", value: "zh-Hans" },
|
||||
{ label: "Türkçe", value: "tr" },
|
||||
{
|
||||
label: "Persian",
|
||||
value: "fa",
|
||||
},
|
||||
].map((preset) => (
|
||||
<SelectItem key={preset.label} value={preset.value}>
|
||||
{preset.label}
|
||||
|
||||
@@ -89,18 +89,14 @@ export const ShowBilling = () => {
|
||||
<div className="pb-5">
|
||||
<Progress value={safePercentage} className="max-w-lg" />
|
||||
</div>
|
||||
{admin && (
|
||||
<>
|
||||
{admin.serversQuantity! <= servers?.length! && (
|
||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
You have reached the maximum number of servers you can
|
||||
create, please upgrade your plan to add more servers.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{admin && admin.serversQuantity! <= servers?.length! && (
|
||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
You have reached the maximum number of servers you can create,
|
||||
please upgrade your plan to add more servers.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -188,7 +184,6 @@ export const ShowBilling = () => {
|
||||
</p>
|
||||
|
||||
<ul
|
||||
role="list"
|
||||
className={clsx(
|
||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||
featured ? "text-white" : "text-slate-200",
|
||||
|
||||
@@ -28,11 +28,7 @@ export const ShowRegistry = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
{data && data?.length > 0 && (
|
||||
<>
|
||||
<AddRegistry />
|
||||
</>
|
||||
)}
|
||||
{data && data?.length > 0 && <AddRegistry />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 pt-4 h-full">
|
||||
|
||||
@@ -34,9 +34,11 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { S3_PROVIDERS } from "./constants";
|
||||
|
||||
const addDestination = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
provider: z.string().optional(),
|
||||
accessKeyId: z.string(),
|
||||
secretAccessKey: z.string(),
|
||||
bucket: z.string(),
|
||||
@@ -58,6 +60,7 @@ export const AddDestination = () => {
|
||||
api.destination.testConnection.useMutation();
|
||||
const form = useForm<AddDestination>({
|
||||
defaultValues: {
|
||||
provider: "",
|
||||
accessKeyId: "",
|
||||
bucket: "",
|
||||
name: "",
|
||||
@@ -73,6 +76,7 @@ export const AddDestination = () => {
|
||||
|
||||
const onSubmit = async (data: AddDestination) => {
|
||||
await mutateAsync({
|
||||
provider: data.provider || "",
|
||||
accessKey: data.accessKeyId,
|
||||
bucket: data.bucket,
|
||||
endpoint: data.endpoint,
|
||||
@@ -123,6 +127,40 @@ export const AddDestination = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="provider"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Provider</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a S3 Provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{S3_PROVIDERS.map((s3Provider) => (
|
||||
<SelectItem
|
||||
key={s3Provider.key}
|
||||
value={s3Provider.key}
|
||||
>
|
||||
{s3Provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -255,6 +293,7 @@ export const AddDestination = () => {
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
provider: form.getValues("provider") || "",
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
@@ -283,6 +322,7 @@ export const AddDestination = () => {
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
provider: form.getValues("provider") || "",
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
export const S3_PROVIDERS: Array<{
|
||||
key: string;
|
||||
name: string;
|
||||
}> = [
|
||||
{
|
||||
key: "AWS",
|
||||
name: "Amazon Web Services (AWS) S3",
|
||||
},
|
||||
{
|
||||
key: "Alibaba",
|
||||
name: "Alibaba Cloud Object Storage System (OSS) formerly Aliyun",
|
||||
},
|
||||
{
|
||||
key: "ArvanCloud",
|
||||
name: "Arvan Cloud Object Storage (AOS)",
|
||||
},
|
||||
{
|
||||
key: "Ceph",
|
||||
name: "Ceph Object Storage",
|
||||
},
|
||||
{
|
||||
key: "ChinaMobile",
|
||||
name: "China Mobile Ecloud Elastic Object Storage (EOS)",
|
||||
},
|
||||
{
|
||||
key: "Cloudflare",
|
||||
name: "Cloudflare R2 Storage",
|
||||
},
|
||||
{
|
||||
key: "DigitalOcean",
|
||||
name: "DigitalOcean Spaces",
|
||||
},
|
||||
{
|
||||
key: "Dreamhost",
|
||||
name: "Dreamhost DreamObjects",
|
||||
},
|
||||
{
|
||||
key: "GCS",
|
||||
name: "Google Cloud Storage",
|
||||
},
|
||||
{
|
||||
key: "HuaweiOBS",
|
||||
name: "Huawei Object Storage Service",
|
||||
},
|
||||
{
|
||||
key: "IBMCOS",
|
||||
name: "IBM COS S3",
|
||||
},
|
||||
{
|
||||
key: "IDrive",
|
||||
name: "IDrive e2",
|
||||
},
|
||||
{
|
||||
key: "IONOS",
|
||||
name: "IONOS Cloud",
|
||||
},
|
||||
{
|
||||
key: "LyveCloud",
|
||||
name: "Seagate Lyve Cloud",
|
||||
},
|
||||
{
|
||||
key: "Leviia",
|
||||
name: "Leviia Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Liara",
|
||||
name: "Liara Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Linode",
|
||||
name: "Linode Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Magalu",
|
||||
name: "Magalu Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Minio",
|
||||
name: "Minio Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Netease",
|
||||
name: "Netease Object Storage (NOS)",
|
||||
},
|
||||
{
|
||||
key: "Petabox",
|
||||
name: "Petabox Object Storage",
|
||||
},
|
||||
{
|
||||
key: "RackCorp",
|
||||
name: "RackCorp Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Rclone",
|
||||
name: "Rclone S3 Server",
|
||||
},
|
||||
{
|
||||
key: "Scaleway",
|
||||
name: "Scaleway Object Storage",
|
||||
},
|
||||
{
|
||||
key: "SeaweedFS",
|
||||
name: "SeaweedFS S3",
|
||||
},
|
||||
{
|
||||
key: "StackPath",
|
||||
name: "StackPath Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Storj",
|
||||
name: "Storj (S3 Compatible Gateway)",
|
||||
},
|
||||
{
|
||||
key: "Synology",
|
||||
name: "Synology C2 Object Storage",
|
||||
},
|
||||
{
|
||||
key: "TencentCOS",
|
||||
name: "Tencent Cloud Object Storage (COS)",
|
||||
},
|
||||
{
|
||||
key: "Wasabi",
|
||||
name: "Wasabi Object Storage",
|
||||
},
|
||||
{
|
||||
key: "Qiniu",
|
||||
name: "Qiniu Object Storage (Kodo)",
|
||||
},
|
||||
{
|
||||
key: "Other",
|
||||
name: "Any other S3 compatible provider",
|
||||
},
|
||||
];
|
||||
@@ -35,9 +35,11 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { S3_PROVIDERS } from "./constants";
|
||||
|
||||
const updateDestination = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
provider: z.string().optional(),
|
||||
accessKeyId: z.string(),
|
||||
secretAccessKey: z.string(),
|
||||
bucket: z.string(),
|
||||
@@ -70,6 +72,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
api.destination.testConnection.useMutation();
|
||||
const form = useForm<UpdateDestination>({
|
||||
defaultValues: {
|
||||
provider: "",
|
||||
accessKeyId: "",
|
||||
bucket: "",
|
||||
name: "",
|
||||
@@ -152,6 +155,40 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="provider"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Provider</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a S3 Provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{S3_PROVIDERS.map((s3Provider) => (
|
||||
<SelectItem
|
||||
key={s3Provider.key}
|
||||
value={s3Provider.key}
|
||||
>
|
||||
{s3Provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -285,6 +322,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
variant={"secondary"}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
provider: form.getValues("provider") || "",
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
@@ -311,6 +349,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
provider: form.getValues("provider") || "",
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
|
||||
@@ -397,25 +397,23 @@ export const AddNotification = () => {
|
||||
)}
|
||||
|
||||
{type === "discord" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "email" && (
|
||||
|
||||
@@ -356,25 +356,23 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
)}
|
||||
|
||||
{type === "discord" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{type === "email" && (
|
||||
<>
|
||||
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { generateSHA256Hash } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -53,6 +54,14 @@ export const ProfileForm = () => {
|
||||
const { data, refetch } = api.auth.get.useQuery();
|
||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||
const { t } = useTranslation("settings");
|
||||
const [gravatarHash, setGravatarHash] = useState<string | null>(null);
|
||||
|
||||
const availableAvatars = useMemo(() => {
|
||||
if (gravatarHash === null) return randomImages;
|
||||
return randomImages.concat([
|
||||
`https://www.gravatar.com/avatar/${gravatarHash}`,
|
||||
]);
|
||||
}, [gravatarHash]);
|
||||
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
@@ -70,6 +79,12 @@ export const ProfileForm = () => {
|
||||
password: "",
|
||||
image: data?.image || "",
|
||||
});
|
||||
|
||||
if (data.email) {
|
||||
generateSHA256Hash(data.email).then((hash) => {
|
||||
setGravatarHash(hash);
|
||||
});
|
||||
}
|
||||
}
|
||||
form.reset();
|
||||
}, [form, form.reset, data]);
|
||||
@@ -154,7 +169,7 @@ export const ProfileForm = () => {
|
||||
value={field.value}
|
||||
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
||||
>
|
||||
{randomImages.map((image) => (
|
||||
{availableAvatars.map((image) => (
|
||||
<FormItem key={image}>
|
||||
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
||||
<FormControl>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ShowServers = () => {
|
||||
)
|
||||
)}
|
||||
{data && data?.length > 0 && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-6 overflow-auto">
|
||||
<Table>
|
||||
<TableCaption>See all servers</TableCaption>
|
||||
<TableHeader>
|
||||
@@ -228,21 +228,17 @@ export const ShowServers = () => {
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
|
||||
{isActive && (
|
||||
{isActive && server.sshKeyId && (
|
||||
<>
|
||||
{server.sshKeyId && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -48,8 +48,8 @@ export const UpdateServer = () => {
|
||||
<li>Some bug that is blocking to use some features</li>
|
||||
</ul>
|
||||
<AlertBlock type="info">
|
||||
Please we recommend to see the latest version to see if there are
|
||||
any breaking changes before updating. Go to{" "}
|
||||
We recommend checking the latest version for any breaking changes
|
||||
before updating. Go to{" "}
|
||||
<Link
|
||||
href="https://github.com/Dokploy/dokploy/releases"
|
||||
target="_blank"
|
||||
|
||||
@@ -231,8 +231,8 @@ export const BitbucketIcon = ({ className }: Props) => {
|
||||
y2="10.814"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset=".18" stop-color="#0052CC" />
|
||||
<stop offset="1" stop-color="#2684FF" />
|
||||
<stop offset=".18" stopColor="#0052CC" />
|
||||
<stop offset="1" stopColor="#2684FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
)}
|
||||
{status === "done" && (
|
||||
<div
|
||||
className={cn("size-3.5 rounded-full bg-primary", className)}
|
||||
className={cn("size-3.5 rounded-full bg-green-500", className)}
|
||||
/>
|
||||
)}
|
||||
{status === "running" && (
|
||||
|
||||
@@ -12,7 +12,7 @@ const buttonVariants = cva(
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/70",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
|
||||
@@ -61,7 +61,7 @@ const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<div ref={ref} {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
));
|
||||
|
||||
1
apps/dokploy/drizzle/0045_smiling_blur.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "destination" ADD COLUMN "provider" text;
|
||||
1
apps/dokploy/drizzle/0046_purple_sleeper.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "registryUrl" text;
|
||||
3981
apps/dokploy/drizzle/meta/0045_snapshot.json
Normal file
3987
apps/dokploy/drizzle/meta/0046_snapshot.json
Normal file
@@ -316,6 +316,20 @@
|
||||
"when": 1731875539532,
|
||||
"tag": "0044_sour_true_believers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 45,
|
||||
"version": "6",
|
||||
"when": 1732644181718,
|
||||
"tag": "0045_smiling_blur",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 46,
|
||||
"version": "6",
|
||||
"when": 1732851191048,
|
||||
"tag": "0046_purple_sleeper",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,3 +4,11 @@ import { twMerge } from "tailwind-merge";
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export async function generateSHA256Hash(text: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "pl", "zh-Hans"],
|
||||
locales: ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
|
||||
localeDetection: false,
|
||||
},
|
||||
fallbackLng: "en",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.12.0",
|
||||
"version": "v0.13.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -32,10 +32,10 @@ const MyApp = ({
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-inter: ${inter.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
:root {
|
||||
--font-inter: ${inter.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
<Head>
|
||||
<title>Dokploy</title>
|
||||
</Head>
|
||||
@@ -71,7 +71,17 @@ export default api.withTRPC(
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "pl", "zh-Hans"],
|
||||
locales: [
|
||||
"en",
|
||||
"pl",
|
||||
"ru",
|
||||
"fr",
|
||||
"de",
|
||||
"tr",
|
||||
"zh-Hant",
|
||||
"zh-Hans",
|
||||
"fa",
|
||||
],
|
||||
localeDetection: false,
|
||||
},
|
||||
fallbackLng: "en",
|
||||
|
||||
@@ -10,137 +10,138 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const signature = req.headers["x-hub-signature-256"];
|
||||
const githubBody = req.body;
|
||||
const signature = req.headers["x-hub-signature-256"];
|
||||
const githubBody = req.body;
|
||||
|
||||
if (!githubBody?.installation?.id) {
|
||||
res.status(400).json({ message: "Github Installation not found" });
|
||||
return;
|
||||
}
|
||||
if (!githubBody?.installation?.id) {
|
||||
res.status(400).json({ message: "Github Installation not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const githubResult = await db.query.github.findFirst({
|
||||
where: eq(github.githubInstallationId, githubBody.installation.id),
|
||||
});
|
||||
const githubResult = await db.query.github.findFirst({
|
||||
where: eq(github.githubInstallationId, githubBody.installation.id),
|
||||
});
|
||||
|
||||
if (!githubResult) {
|
||||
res.status(400).json({ message: "Github Installation not found" });
|
||||
return;
|
||||
}
|
||||
if (!githubResult) {
|
||||
res.status(400).json({ message: "Github Installation not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!githubResult.githubWebhookSecret) {
|
||||
res.status(400).json({ message: "Github Webhook Secret not set" });
|
||||
return;
|
||||
}
|
||||
const webhooks = new Webhooks({
|
||||
secret: githubResult.githubWebhookSecret,
|
||||
});
|
||||
if (!githubResult.githubWebhookSecret) {
|
||||
res.status(400).json({ message: "Github Webhook Secret not set" });
|
||||
return;
|
||||
}
|
||||
const webhooks = new Webhooks({
|
||||
secret: githubResult.githubWebhookSecret,
|
||||
});
|
||||
|
||||
const verified = await webhooks.verify(
|
||||
JSON.stringify(githubBody),
|
||||
signature as string,
|
||||
);
|
||||
const verified = await webhooks.verify(
|
||||
JSON.stringify(githubBody),
|
||||
signature as string
|
||||
);
|
||||
|
||||
if (!verified) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
if (!verified) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers["x-github-event"] === "ping") {
|
||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||
return;
|
||||
}
|
||||
if (req.headers["x-github-event"] === "ping") {
|
||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers["x-github-event"] !== "push") {
|
||||
res.status(400).json({ message: "We only accept push events" });
|
||||
return;
|
||||
}
|
||||
if (req.headers["x-github-event"] !== "push") {
|
||||
res.status(400).json({ message: "We only accept push events" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
||||
const repository = githubBody?.repository?.name;
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
try {
|
||||
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
||||
const repository = githubBody?.repository?.name;
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
|
||||
const apps = await db.query.applications.findMany({
|
||||
where: and(
|
||||
eq(applications.sourceType, "github"),
|
||||
eq(applications.autoDeploy, true),
|
||||
eq(applications.branch, branchName),
|
||||
eq(applications.repository, repository),
|
||||
),
|
||||
});
|
||||
const apps = await db.query.applications.findMany({
|
||||
where: and(
|
||||
eq(applications.sourceType, "github"),
|
||||
eq(applications.autoDeploy, true),
|
||||
eq(applications.branch, branchName),
|
||||
eq(applications.repository, repository)
|
||||
),
|
||||
});
|
||||
|
||||
for (const app of apps) {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
for (const app of apps) {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: app.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
await deploy(jobData);
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
await deploy(jobData);
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const composeApps = await db.query.compose.findMany({
|
||||
where: and(
|
||||
eq(compose.sourceType, "github"),
|
||||
eq(compose.autoDeploy, true),
|
||||
eq(compose.branch, branchName),
|
||||
eq(compose.repository, repository),
|
||||
),
|
||||
});
|
||||
const composeApps = await db.query.compose.findMany({
|
||||
where: and(
|
||||
eq(compose.sourceType, "github"),
|
||||
eq(compose.autoDeploy, true),
|
||||
eq(compose.branch, branchName),
|
||||
eq(compose.repository, repository)
|
||||
),
|
||||
});
|
||||
|
||||
for (const composeApp of composeApps) {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
};
|
||||
for (const composeApp of composeApps) {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: composeApp.composeId as string,
|
||||
titleLog: deploymentTitle,
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
server: !!composeApp.serverId,
|
||||
};
|
||||
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
await deploy(jobData);
|
||||
return true;
|
||||
}
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
await deploy(jobData);
|
||||
return true;
|
||||
}
|
||||
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const totalApps = apps.length + composeApps.length;
|
||||
const emptyApps = totalApps === 0;
|
||||
const totalApps = apps.length + composeApps.length;
|
||||
const emptyApps = totalApps === 0;
|
||||
|
||||
if (emptyApps) {
|
||||
res.status(200).json({ message: "No apps to deploy" });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Error To Deploy Application", error });
|
||||
}
|
||||
if (emptyApps) {
|
||||
res.status(200).json({ message: "No apps to deploy" });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Error To Deploy Application", error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AddApplication } from "@/components/dashboard/project/add-application";
|
||||
import { AddCompose } from "@/components/dashboard/project/add-compose";
|
||||
import { AddDatabase } from "@/components/dashboard/project/add-database";
|
||||
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
||||
import { ProjectEnviroment } from "@/components/dashboard/projects/project-enviroment";
|
||||
import {
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
@@ -198,27 +199,35 @@ const Project = (
|
||||
</div>
|
||||
|
||||
{(auth?.rol === "admin" || user?.canCreateServices) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Service
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[200px] space-y-2" align="end">
|
||||
<DropdownMenuLabel className="text-sm font-normal ">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<AddApplication
|
||||
projectId={projectId}
|
||||
projectName={data?.name}
|
||||
/>
|
||||
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||
<AddTemplate projectId={projectId} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<ProjectEnviroment projectId={projectId}>
|
||||
<Button variant="outline">Project Enviroment</Button>
|
||||
</ProjectEnviroment>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Service
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[200px] space-y-2"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel className="text-sm font-normal ">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<AddApplication
|
||||
projectId={projectId}
|
||||
projectName={data?.name}
|
||||
/>
|
||||
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||
<AddTemplate projectId={projectId} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||
import { ShowWelcomeDokploy } from "@/components/dashboard/settings/billing/show-welcome-dokploy";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const ShowWelcomeDokploy = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/settings/billing/show-welcome-dokploy").then(
|
||||
(mod) => mod.ShowWelcomeDokploy,
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const Dashboard = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<>
|
||||
<ShowWelcomeDokploy />
|
||||
{isCloud && <ShowWelcomeDokploy />}
|
||||
|
||||
<ShowProjects />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import nextI18NextConfig from "../../../next-i18next.config.cjs";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
|
||||
1
apps/dokploy/public/locales/de/common.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
44
apps/dokploy/public/locales/de/settings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"settings.common.save": "Speichern",
|
||||
"settings.server.domain.title": "Server-Domain",
|
||||
"settings.server.domain.description": "Füg eine Domain zu deiner Server-Anwendung hinzu.",
|
||||
"settings.server.domain.form.domain": "Domain",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-Mail",
|
||||
"settings.server.domain.form.certificate.label": "Zertifikat",
|
||||
"settings.server.domain.form.certificate.placeholder": "Wähl ein Zertifikat aus",
|
||||
"settings.server.domain.form.certificateOptions.none": "Keins",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Standard)",
|
||||
|
||||
"settings.server.webServer.title": "Web-Server",
|
||||
"settings.server.webServer.description": "Lade den Web-Server neu oder reinige ihn.",
|
||||
"settings.server.webServer.actions": "Aktionen",
|
||||
"settings.server.webServer.reload": "Neu laden",
|
||||
"settings.server.webServer.watchLogs": "Logs anschauen",
|
||||
"settings.server.webServer.updateServerIp": "Server-IP Aktualisieren",
|
||||
"settings.server.webServer.server.label": "Server",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Umgebungsvariablen ändern",
|
||||
"settings.server.webServer.storage.label": "Speicherplatz",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Nicht genutzte Bilder löschen",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Nicht genutzte Volumes löschen",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "Gestoppte Container löschen",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & System bereinigen",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "Monitoring bereinigen",
|
||||
"settings.server.webServer.storage.cleanAll": "Alles bereinigen",
|
||||
|
||||
"settings.profile.title": "Konto",
|
||||
"settings.profile.description": "Ändere die Details deines Profiles hier.",
|
||||
"settings.profile.email": "E-Mail",
|
||||
"settings.profile.password": "Passwort",
|
||||
"settings.profile.avatar": "Avatar",
|
||||
|
||||
"settings.appearance.title": "Aussehen",
|
||||
"settings.appearance.description": "Pass das Design deines Dashboards an.",
|
||||
"settings.appearance.theme": "Theme",
|
||||
"settings.appearance.themeDescription": "Wähl ein Theme für dein Dashboard aus",
|
||||
"settings.appearance.themes.light": "Hell",
|
||||
"settings.appearance.themes.dark": "Dunkel",
|
||||
"settings.appearance.themes.system": "System",
|
||||
"settings.appearance.language": "Sprache",
|
||||
"settings.appearance.languageDescription": "Wähl eine Sprache für dein Dashboard aus"
|
||||
}
|
||||
1
apps/dokploy/public/locales/fa/common.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
44
apps/dokploy/public/locales/fa/settings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"settings.common.save": "ذخیره",
|
||||
"settings.server.domain.title": "دامنه سرور",
|
||||
"settings.server.domain.description": "یک دامنه به برنامه سرور خود اضافه کنید.",
|
||||
"settings.server.domain.form.domain": "دامنه",
|
||||
"settings.server.domain.form.letsEncryptEmail": "ایمیل Let's Encrypt",
|
||||
"settings.server.domain.form.certificate.label": "گواهینامه",
|
||||
"settings.server.domain.form.certificate.placeholder": "یک گواهینامه انتخاب کنید",
|
||||
"settings.server.domain.form.certificateOptions.none": "هیچکدام",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (پیشفرض)",
|
||||
|
||||
"settings.server.webServer.title": "وب سرور",
|
||||
"settings.server.webServer.description": "وب سرور را بازنشانی یا پاک کنید.",
|
||||
"settings.server.webServer.actions": "اقدامات",
|
||||
"settings.server.webServer.reload": "بارگذاری مجدد",
|
||||
"settings.server.webServer.watchLogs": "مشاهده گزارشها",
|
||||
"settings.server.webServer.updateServerIp": "بهروزرسانی آیپی سرور",
|
||||
"settings.server.webServer.server.label": "سرور",
|
||||
"settings.server.webServer.traefik.label": "ترافیک",
|
||||
"settings.server.webServer.traefik.modifyEnv": "ویرایش محیط",
|
||||
"settings.server.webServer.storage.label": "فضا",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "پاکسازی Image های بدون استفاده",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "پاکسازی ولومهای بدون استفاده",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "پاکسازی کانتینرهای متوقفشده",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "پاکسازی بیلدر و سیستم داکر",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "پاکسازی پایش",
|
||||
"settings.server.webServer.storage.cleanAll": "پاکسازی همه",
|
||||
|
||||
"settings.profile.title": "حساب کاربری",
|
||||
"settings.profile.description": "جزئیات پروفایل خود را در اینجا تغییر دهید.",
|
||||
"settings.profile.email": "ایمیل",
|
||||
"settings.profile.password": "رمز عبور",
|
||||
"settings.profile.avatar": "تصویر پروفایل",
|
||||
|
||||
"settings.appearance.title": "ظاهر",
|
||||
"settings.appearance.description": "تم داشبورد خود را سفارشی کنید.",
|
||||
"settings.appearance.theme": "تم",
|
||||
"settings.appearance.themeDescription": "یک تم برای داشبورد خود انتخاب کنید",
|
||||
"settings.appearance.themes.light": "روشن",
|
||||
"settings.appearance.themes.dark": "تاریک",
|
||||
"settings.appearance.themes.system": "سیستم",
|
||||
"settings.appearance.language": "زبان",
|
||||
"settings.appearance.languageDescription": "یک زبان برای داشبورد خود انتخاب کنید"
|
||||
}
|
||||
1
apps/dokploy/public/locales/fr/common.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
44
apps/dokploy/public/locales/fr/settings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"settings.common.save": "Sauvegarder",
|
||||
"settings.server.domain.title": "Nom de domaine du serveur",
|
||||
"settings.server.domain.description": "Ajouter un nom de domaine au serveur de votre application.",
|
||||
"settings.server.domain.form.domain": "Domaine",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Adresse email Let's Encrypt",
|
||||
"settings.server.domain.form.certificate.label": "Certificat",
|
||||
"settings.server.domain.form.certificate.placeholder": "Choisir un certificat",
|
||||
"settings.server.domain.form.certificateOptions.none": "Aucun",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Par défaut)",
|
||||
|
||||
"settings.server.webServer.title": "Serveur web",
|
||||
"settings.server.webServer.description": "Recharger ou nettoyer le serveur web.",
|
||||
"settings.server.webServer.actions": "Actions",
|
||||
"settings.server.webServer.reload": "Recharger",
|
||||
"settings.server.webServer.watchLogs": "Consulter les logs",
|
||||
"settings.server.webServer.updateServerIp": "Mettre à jour l'IP du serveur",
|
||||
"settings.server.webServer.server.label": "Serveur",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Modifier les variables d'environnement",
|
||||
"settings.server.webServer.storage.label": "Stockage",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Supprimer les images inutilisées",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Supprimer les volumes inutilisés",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "Supprimer les conteneurs arrêtés",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "Nettoyer le Docker Builder & System",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "Nettoyer le monitoring",
|
||||
"settings.server.webServer.storage.cleanAll": "Tout nettoyer",
|
||||
|
||||
"settings.profile.title": "Compte",
|
||||
"settings.profile.description": "Modifier les informations de votre compte ici.",
|
||||
"settings.profile.email": "Adresse Email",
|
||||
"settings.profile.password": "Mot de passe",
|
||||
"settings.profile.avatar": "Photo de profil",
|
||||
|
||||
"settings.appearance.title": "Apparence",
|
||||
"settings.appearance.description": "Customiser le thème de votre dashboard.",
|
||||
"settings.appearance.theme": "Thème",
|
||||
"settings.appearance.themeDescription": "Choisir un thème pour votre dashboard",
|
||||
"settings.appearance.themes.light": "Clair",
|
||||
"settings.appearance.themes.dark": "Sombre",
|
||||
"settings.appearance.themes.system": "Système",
|
||||
"settings.appearance.language": "Langue",
|
||||
"settings.appearance.languageDescription": "Sélectionner une langue pour votre dashboard"
|
||||
}
|
||||
1
apps/dokploy/public/locales/ru/common.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
44
apps/dokploy/public/locales/ru/settings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"settings.common.save": "Сохранить",
|
||||
"settings.server.domain.title": "Домен сервера",
|
||||
"settings.server.domain.description": "Установите домен для вашего серверного приложения Dokploy.",
|
||||
"settings.server.domain.form.domain": "Домен",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Email для Let's Encrypt",
|
||||
"settings.server.domain.form.certificate.label": "Сертификат",
|
||||
"settings.server.domain.form.certificate.placeholder": "Выберите сертификат",
|
||||
"settings.server.domain.form.certificateOptions.none": "Нет",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (По умолчанию)",
|
||||
|
||||
"settings.server.webServer.title": "Веб-сервер",
|
||||
"settings.server.webServer.description": "Перезагрузка или очистка веб-сервера.",
|
||||
"settings.server.webServer.server.label": "Сервер",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.storage.label": "Дисковое пространство",
|
||||
"settings.server.webServer.actions": "Действия",
|
||||
"settings.server.webServer.reload": "Перезагрузить",
|
||||
"settings.server.webServer.watchLogs": "Просмотр логов",
|
||||
"settings.server.webServer.updateServerIp": "Изменить IP адрес",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Изменить переменные окружения",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Очистить неиспользуемые образы",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистить неиспользуемые тома",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "Очистить остановленные контейнеры",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "Очистить Docker Builder и систему",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "Очистить мониторинг",
|
||||
"settings.server.webServer.storage.cleanAll": "Очистить все",
|
||||
|
||||
"settings.profile.title": "Аккаунт",
|
||||
"settings.profile.description": "Измените данные вашего профиля.",
|
||||
"settings.profile.email": "Email",
|
||||
"settings.profile.password": "Пароль",
|
||||
"settings.profile.avatar": "Аватар",
|
||||
|
||||
"settings.appearance.title": "Внешний вид",
|
||||
"settings.appearance.description": "Настройте тему Dokploy.",
|
||||
"settings.appearance.theme": "Тема",
|
||||
"settings.appearance.themeDescription": "Выберите тему системной панели",
|
||||
"settings.appearance.themes.light": "Светлая",
|
||||
"settings.appearance.themes.dark": "Темная",
|
||||
"settings.appearance.themes.system": "Системная",
|
||||
"settings.appearance.language": "Язык",
|
||||
"settings.appearance.languageDescription": "Select a language for your dashboard"
|
||||
}
|
||||
1
apps/dokploy/public/locales/tr/common.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
44
apps/dokploy/public/locales/tr/settings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"settings.common.save": "Kaydet",
|
||||
"settings.server.domain.title": "Sunucu Alanı",
|
||||
"settings.server.domain.description": "Sunucu uygulamanıza bir alan adı ekleyin.",
|
||||
"settings.server.domain.form.domain": "Alan Adı",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-postası",
|
||||
"settings.server.domain.form.certificate.label": "Sertifika",
|
||||
"settings.server.domain.form.certificate.placeholder": "Bir sertifika seçin",
|
||||
"settings.server.domain.form.certificateOptions.none": "Hiçbiri",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Varsayılan)",
|
||||
|
||||
"settings.server.webServer.title": "Web Sunucusu",
|
||||
"settings.server.webServer.description": "Web sunucusunu yeniden yükleyin veya temizleyin.",
|
||||
"settings.server.webServer.actions": "İşlemler",
|
||||
"settings.server.webServer.reload": "Yeniden Yükle",
|
||||
"settings.server.webServer.watchLogs": "Günlükleri İzle",
|
||||
"settings.server.webServer.updateServerIp": "Sunucu IP'sini Güncelle",
|
||||
"settings.server.webServer.server.label": "Sunucu",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "Env Değiştir",
|
||||
"settings.server.webServer.storage.label": "Alan",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "Kullanılmayan görüntüleri temizle",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Kullanılmayan birimleri temizle",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "Durmuş konteynerleri temizle",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder ve Sistemi Temizle",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "İzlemeyi Temizle",
|
||||
"settings.server.webServer.storage.cleanAll": "Hepsini temizle",
|
||||
|
||||
"settings.profile.title": "Hesap",
|
||||
"settings.profile.description": "Profil detaylarınızı buradan değiştirebilirsiniz.",
|
||||
"settings.profile.email": "E-posta",
|
||||
"settings.profile.password": "Şifre",
|
||||
"settings.profile.avatar": "Profil Resmi",
|
||||
|
||||
"settings.appearance.title": "Görünüm",
|
||||
"settings.appearance.description": "Kontrol panelinin temasını özelleştirin.",
|
||||
"settings.appearance.theme": "Tema",
|
||||
"settings.appearance.themeDescription": "Kontrol paneli için bir tema seçin",
|
||||
"settings.appearance.themes.light": "Açık",
|
||||
"settings.appearance.themes.dark": "Koyu",
|
||||
"settings.appearance.themes.system": "Sistem",
|
||||
"settings.appearance.language": "Dil",
|
||||
"settings.appearance.languageDescription": "Kontrol paneli için bir dil seçin"
|
||||
}
|
||||
1
apps/dokploy/public/locales/zh-Hant/common.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
40
apps/dokploy/public/locales/zh-Hant/settings.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"settings.common.save": "儲存",
|
||||
"settings.server.domain.title": "伺服器網域",
|
||||
"settings.server.domain.description": "將一個網域加入到您的伺服器。",
|
||||
"settings.server.domain.form.domain": "網域",
|
||||
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 郵箱",
|
||||
"settings.server.domain.form.certificate.label": "憑證",
|
||||
"settings.server.domain.form.certificate.placeholder": "選擇一個憑證",
|
||||
"settings.server.domain.form.certificateOptions.none": "無",
|
||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (預設)",
|
||||
"settings.server.webServer.title": "Web 伺服器",
|
||||
"settings.server.webServer.description": "管理 Web 伺服器。",
|
||||
"settings.server.webServer.actions": "操作",
|
||||
"settings.server.webServer.reload": "重新載入",
|
||||
"settings.server.webServer.watchLogs": "查看日誌",
|
||||
"settings.server.webServer.server.label": "伺服器",
|
||||
"settings.server.webServer.traefik.label": "Traefik",
|
||||
"settings.server.webServer.traefik.modifyEnv": "修改環境變數",
|
||||
"settings.server.webServer.storage.label": "磁碟空間",
|
||||
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的映像檔",
|
||||
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的磁碟區",
|
||||
"settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器",
|
||||
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系統快取",
|
||||
"settings.server.webServer.storage.cleanMonitoring": "清理監控",
|
||||
"settings.server.webServer.storage.cleanAll": "清理所有",
|
||||
"settings.profile.title": "帳戶偏好",
|
||||
"settings.profile.description": "更改您的個人資料詳情。",
|
||||
"settings.profile.email": "電子郵件",
|
||||
"settings.profile.password": "密碼",
|
||||
"settings.profile.avatar": "頭像",
|
||||
"settings.appearance.title": "外觀",
|
||||
"settings.appearance.description": "自訂儀表板主題。",
|
||||
"settings.appearance.theme": "主題",
|
||||
"settings.appearance.themeDescription": "選擇儀表板主題",
|
||||
"settings.appearance.themes.light": "亮",
|
||||
"settings.appearance.themes.dark": "暗",
|
||||
"settings.appearance.themes.system": "系統",
|
||||
"settings.appearance.language": "語言",
|
||||
"settings.appearance.languageDescription": "選擇儀表板語言"
|
||||
}
|
||||
3
apps/dokploy/public/templates/chatwoot.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="undefined" height="undefined" viewBox="0 0 24 24">
|
||||
<path fill="#0ea5e9" d="M0 12c0 6.629 5.371 12 12 12s12-5.371 12-12S18.629 0 12 0S0 5.371 0 12m17.008 5.29H11.44a5.57 5.57 0 0 1-5.562-5.567A5.57 5.57 0 0 1 11.44 6.16a5.57 5.57 0 0 1 5.567 5.563Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
8
apps/dokploy/public/templates/discourse.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 104 106">
|
||||
<path fill="#231f20" d="M51.87 0C23.71 0 0 22.83 0 51v52.81l51.86-.05c28.16 0 51-23.71 51-51.87S80 0 51.87 0Z"/>
|
||||
<path fill="#fff9ae" d="M52.37 19.74a31.62 31.62 0 0 0-27.79 46.67l-5.72 18.4 20.54-4.64a31.61 31.61 0 1 0 13-60.43Z"/>
|
||||
<path fill="#00aeef" d="M77.45 32.12a31.6 31.6 0 0 1-38.05 48l-20.54 4.7 20.91-2.47a31.6 31.6 0 0 0 37.68-50.23Z"/>
|
||||
<path fill="#00a94f" d="M71.63 26.29A31.6 31.6 0 0 1 38.8 78l-19.94 6.82 20.54-4.65a31.6 31.6 0 0 0 32.23-53.88Z"/>
|
||||
<path fill="#f15d22" d="M26.47 67.11a31.61 31.61 0 0 1 51-35 31.61 31.61 0 0 0-52.89 34.3l-5.72 18.4Z"/>
|
||||
<path fill="#e31b23" d="M24.58 66.41a31.61 31.61 0 0 1 47.05-40.12 31.61 31.61 0 0 0-49 39.63l-3.76 18.9Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 761 B |
5
apps/dokploy/public/templates/heyform.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="130" height="130" viewBox="0 0 130 130" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.1957 12.5861C65.1413 14.0499 66.2222 16.0447 66.4383 18.5707L66.4722 18.5513L66.4721 19.1439C66.4765 19.2753 66.4787 19.408 66.4787 19.5421L66.4721 19.5457L66.4721 70.8151C66.5176 73.4895 67.517 75.6073 69.4703 77.1684C72.3982 79.2596 75.4512 79.454 78.6294 77.7515L37.0683 101.69C33.9831 103.279 31.0322 103.141 28.2155 101.276C26.274 99.8155 25.1936 97.826 24.9743 95.3078L24.9325 95.3318L24.9326 42.812C24.8338 40.2483 23.8366 38.2089 21.9409 36.6938C21.0056 36.0258 20.0575 35.5513 19.0967 35.2704C17.732 34.9092 17.732 33.2068 18.738 32.6801C21.0305 31.3597 32.8988 24.5238 54.3429 12.1726C57.4281 10.5835 60.3791 10.7214 63.1957 12.5861Z" fill="#9DE8FF"/>
|
||||
<path d="M66.472 70.8079C66.4429 65.6445 62.2284 58.1373 53.6907 58.1373C51.3183 58.1373 49.1069 58.7573 47.0566 59.9971L85.1604 38.0562C87.0493 36.9588 89.2462 36.3301 91.5906 36.3301C98.621 36.3301 104.326 41.9843 104.372 48.9767C104.372 48.9781 104.372 49.0065 104.372 49.0621L104.372 87.79C104.418 90.4635 105.417 92.5806 107.37 94.1413C108.316 94.8171 109.275 95.2948 110.248 95.5744C111.649 96.0291 111.488 97.668 110.58 98.1513C105.284 101.202 94.6267 107.35 78.608 116.598C75.5228 118.187 72.5719 118.049 69.7552 116.184C67.8089 114.72 66.728 112.724 66.5124 110.197L66.4722 110.22V100.642C66.4721 90.69 66.4721 70.8199 66.472 70.8079Z" fill="#106BF3"/>
|
||||
<path d="M53.6907 58.1373C60.7497 58.1373 66.4722 63.8376 66.4722 70.8692C66.4722 77.9274 66.4722 82.5578 66.4722 84.7602C66.4722 84.7602 44.4801 97.4179 41.2926 99.2526C41.0435 99.3959 36.9265 101.759 36.9265 101.759C40.583 99.3959 40.9093 96.2258 40.9093 94.8181C40.9093 90.7365 40.9093 82.7535 40.9093 70.8692C40.9093 63.8376 46.6317 58.1373 53.6907 58.1373Z" fill="#57AAF9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
9
apps/dokploy/public/templates/immich.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.417391" y="0.417391" width="95.1652" height="95.1652" rx="47.5826" fill="white"/>
|
||||
<rect x="0.417391" y="0.417391" width="95.1652" height="95.1652" rx="47.5826" stroke="#E9EAEC" stroke-width="0.834783"/>
|
||||
<path d="M45.416 33.5745C50.1219 37.7635 53.9144 42.2526 56.3551 46.4832C60.5471 38.9452 63.3482 29.9879 63.3835 24.2829C63.3835 24.2425 63.3835 24.2057 63.3835 24.1715C63.3835 15.7297 55.0081 12.4443 47.7933 12.4443C40.5786 12.4443 32.2031 15.7297 32.2031 24.1715C32.2031 24.2866 32.2031 24.4409 32.2031 24.627C36.2246 26.4246 40.9914 29.6364 45.416 33.5745Z" fill="#FA2921"/>
|
||||
<path d="M19.7443 56.5951C22.6855 53.3048 27.1978 49.739 32.2898 46.7243C37.707 43.5185 43.1254 41.2789 47.8812 40.254C42.0463 33.9159 34.4394 28.4693 29.0527 26.673C29.0149 26.6607 28.9796 26.6497 28.9479 26.6387C20.9622 24.0305 15.2661 31.0236 13.0374 37.9225C10.8087 44.8214 11.3275 53.846 19.3132 56.4542C19.4216 56.4897 19.5677 56.5375 19.7443 56.5951Z" fill="#ED79B5"/>
|
||||
<path d="M82.6341 37.8063C80.4054 30.9074 74.7093 23.9143 66.7236 26.5225C66.614 26.558 66.4679 26.6057 66.2925 26.6633C65.8358 31.0629 64.2708 36.6149 61.9129 42.0627C59.4053 47.8571 56.3301 52.8715 53.082 56.5119C61.5074 58.1919 70.8462 58.1013 76.2536 56.3723C76.2914 56.3601 76.3267 56.3478 76.3583 56.338C84.344 53.7286 84.8629 44.704 82.6341 37.8063Z" fill="#FFB400"/>
|
||||
<path d="M40.6729 63.3397C39.315 57.1694 38.8704 51.2954 39.3698 46.4316C31.5716 50.0525 24.0694 55.6436 20.7044 60.2392C20.6812 60.271 20.6593 60.3017 20.6398 60.3286C15.7049 67.1589 20.5595 74.7668 26.3968 79.0293C32.2329 83.293 40.9299 85.5853 45.866 78.755C45.9342 78.6619 46.0243 78.537 46.1327 78.3864C43.9295 74.5574 41.9493 69.1402 40.6729 63.3397Z" fill="#1E83F7"/>
|
||||
<path d="M74.9007 59.8885C70.5979 60.8118 64.8628 61.031 58.9804 60.4591C52.7241 59.8518 47.0317 58.4607 42.584 56.4795C43.5985 65.0547 46.5701 73.9569 49.8767 78.5941C49.8998 78.626 49.9218 78.6566 49.9413 78.6835C54.8761 85.5138 63.5731 83.2215 69.4104 78.9578C75.2466 74.6941 80.1023 67.0862 75.1674 60.2571C75.0992 60.164 75.0091 60.0391 74.9007 59.8885Z" fill="#18C249"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/dokploy/public/templates/ontime.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
1
apps/dokploy/public/templates/photoprism.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 266 266"><defs><linearGradient id="a" x1="45.04" y1="231.72" x2="231.72" y2="45.04" gradientUnits="userSpaceOnUse" gradientTransform="translate(-5.38 -5.38)"><stop offset="0" stop-color="#fff"/><stop offset="0" stop-color="#b8edff"/><stop offset="1" stop-color="#d4b8ff"/></linearGradient></defs><circle cx="133" cy="133" r="132" style="fill:url(#a)"/><path data-name="Logo Pfad" d="m224.19 176.51-4 24.19M41.91 177.5l14.81 14m95.76-137.65L56.62 191.31a.09.09 0 0 0 .07.15l163.41 9.37a.09.09 0 0 0 .09-.13L152.62 53.87a.1.1 0 0 0-.14-.02zm-19.74-13.29L41.8 177.31a.13.13 0 0 0 .11.19l182.18-.8a.12.12 0 0 0 .1-.19L132.95 40.56a.12.12 0 0 0-.21 0zm.11-.16 19.77 13.32" style="fill:none;stroke:#1d1d1b;stroke-miterlimit:10;stroke-width:6px"/></svg>
|
||||
|
After Width: | Height: | Size: 819 B |
BIN
apps/dokploy/public/templates/ryot.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
6
apps/dokploy/public/templates/twenty.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40">
|
||||
<g id="ss11151339769_1">
|
||||
<path d="M 0 40 L 0 0 L 40 0 L 40 40 Z" fill="transparent"></path>
|
||||
<path d="M 34.95 0 L 5.05 0 C 2.262 0 0 2.262 0 5.05 L 0 34.95 C 0 37.738 2.262 40 5.05 40 L 34.95 40 C 37.738 40 40 37.738 40 34.95 L 40 5.05 C 40 2.262 37.738 0 34.95 0 Z M 8.021 14.894 C 8.021 12.709 9.794 10.935 11.979 10.935 L 19.6 10.935 C 19.712 10.935 19.815 11.003 19.862 11.106 C 19.909 11.209 19.888 11.329 19.812 11.415 L 18.141 13.229 C 17.85 13.544 17.441 13.726 17.012 13.726 L 12 13.726 C 11.344 13.726 10.812 14.259 10.812 14.915 L 10.812 17.909 C 10.812 18.294 10.5 18.606 10.115 18.606 L 8.721 18.606 C 8.335 18.606 8.024 18.294 8.024 17.909 L 8.024 14.894 Z M 31.729 25.106 C 31.729 27.291 29.956 29.065 27.771 29.065 L 24.532 29.065 C 22.347 29.065 20.574 27.291 20.574 25.106 L 20.574 19.438 C 20.574 19.053 20.718 18.682 20.979 18.397 L 22.868 16.347 C 22.947 16.262 23.071 16.232 23.182 16.274 C 23.291 16.318 23.365 16.421 23.365 16.538 L 23.365 25.088 C 23.365 25.744 23.897 26.276 24.553 26.276 L 27.753 26.276 C 28.409 26.276 28.941 25.744 28.941 25.088 L 28.941 14.915 C 28.941 14.259 28.409 13.726 27.753 13.726 L 24.032 13.726 C 23.606 13.726 23.2 13.906 22.909 14.218 L 11.812 26.276 L 18.479 26.276 C 18.865 26.276 19.176 26.588 19.176 26.974 L 19.176 28.368 C 19.176 28.753 18.865 29.065 18.479 29.065 L 9.494 29.065 C 8.679 29.065 8.018 28.403 8.018 27.588 L 8.018 26.85 C 8.018 26.479 8.156 26.124 8.409 25.85 L 20.85 12.335 C 21.674 11.441 22.829 10.935 24.044 10.935 L 27.768 10.935 C 29.953 10.935 31.726 12.709 31.726 14.894 L 31.726 25.106 Z" fill="rgb(0,0,0)"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
apps/dokploy/public/templates/yourls.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -384,6 +384,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
password: input.password,
|
||||
sourceType: "docker",
|
||||
applicationStatus: "idle",
|
||||
registryUrl: input.registryUrl,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -79,6 +79,8 @@ export const composeRouter = createTRPCRouter({
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newService.composeId);
|
||||
}
|
||||
|
||||
return newService;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,10 @@ export const destinationRouter = createTRPCRouter({
|
||||
testConnection: adminProcedure
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
||||
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
|
||||
input;
|
||||
try {
|
||||
const rcloneFlags = [
|
||||
// `--s3-provider=Cloudflare`,
|
||||
`--s3-access-key-id=${accessKey}`,
|
||||
`--s3-secret-access-key=${secretAccessKey}`,
|
||||
`--s3-region=${region}`,
|
||||
@@ -52,6 +51,9 @@ export const destinationRouter = createTRPCRouter({
|
||||
"--s3-no-check-bucket",
|
||||
"--s3-force-path-style",
|
||||
];
|
||||
if (provider) {
|
||||
rcloneFlags.unshift(`--s3-provider=${provider}`);
|
||||
}
|
||||
const rcloneDestination = `:s3:${bucket}`;
|
||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
|
||||
@@ -3,173 +3,173 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
--overlay: rgba(0, 0, 0, 0.2);
|
||||
--radius: 0.5rem;
|
||||
--overlay: rgba(0, 0, 0, 0.2);
|
||||
|
||||
--chart-1: 173 58% 39%;
|
||||
--chart-2: 12 76% 61%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
--chart-1: 173 58% 39%;
|
||||
--chart-2: 12 76% 61%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 98%;
|
||||
.dark {
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 4% 10%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--card: 240 4% 10%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 4% 10%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--muted: 240 4% 10%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 4% 10%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 4% 10%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--overlay: rgba(0, 0, 0, 0.5);
|
||||
--overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-5: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-2: 340 75% 55%;
|
||||
}
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-5: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-2: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.xterm-viewport {
|
||||
border-radius: 0.75rem /* 12px */ !important;
|
||||
border-radius: 0.75rem /* 12px */ !important;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
overflow-y: auto !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
/* Codemirror */
|
||||
.cm-editor {
|
||||
@apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none;
|
||||
@apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none;
|
||||
}
|
||||
|
||||
.cm-editor .cm-scroller {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
@apply outline-none;
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
/* fix: placeholder bg */
|
||||
.cm-editor .cm-activeLine:has(.cm-placeholder) {
|
||||
background-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.compose-file-editor .cm-editor {
|
||||
@apply min-h-[25rem];
|
||||
@apply min-h-[25rem];
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
75% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
75% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-heartbeat {
|
||||
animation: heartbeat 2.5s infinite;
|
||||
animation: heartbeat 2.5s infinite;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.swagger-ui {
|
||||
background-color: white;
|
||||
}
|
||||
.swagger-ui {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.swagger-ui .info {
|
||||
margin: 0px !important;
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
.swagger-ui .info {
|
||||
margin: 0px !important;
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
76
apps/dokploy/templates/chatwoot/docker-compose.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
version: '3'
|
||||
|
||||
x-base-config: &base-config
|
||||
image: chatwoot/chatwoot:v3.14.1
|
||||
volumes:
|
||||
- chatwoot-storage:/app/storage
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
|
||||
- RAILS_ENV=${RAILS_ENV}
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
- INSTALLATION_ENV=${INSTALLATION_ENV}
|
||||
- RAILS_LOG_TO_STDOUT=${RAILS_LOG_TO_STDOUT}
|
||||
- LOG_LEVEL=${LOG_LEVEL}
|
||||
- DEFAULT_LOCALE=${DEFAULT_LOCALE}
|
||||
- POSTGRES_HOST=${POSTGRES_HOST}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT}
|
||||
- POSTGRES_DATABASE=${POSTGRES_DATABASE}
|
||||
- POSTGRES_USERNAME=${POSTGRES_USERNAME}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- REDIS_URL=${REDIS_URL}
|
||||
- ENABLE_ACCOUNT_SIGNUP=${ENABLE_ACCOUNT_SIGNUP}
|
||||
- ACTIVE_STORAGE_SERVICE=${ACTIVE_STORAGE_SERVICE}
|
||||
|
||||
services:
|
||||
chatwoot-rails:
|
||||
<<: *base-config
|
||||
depends_on:
|
||||
chatwoot-postgres:
|
||||
condition: service_started
|
||||
chatwoot-redis:
|
||||
condition: service_started
|
||||
entrypoint: docker/entrypoints/rails.sh
|
||||
command: ['bundle', 'exec', 'sh', '-c', 'rails db:chatwoot_prepare && rails s -p 3000 -b 0.0.0.0']
|
||||
restart: always
|
||||
|
||||
chatwoot-sidekiq:
|
||||
<<: *base-config
|
||||
depends_on:
|
||||
chatwoot-postgres:
|
||||
condition: service_started
|
||||
chatwoot-redis:
|
||||
condition: service_started
|
||||
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
|
||||
restart: always
|
||||
|
||||
chatwoot-postgres:
|
||||
image: postgres:12
|
||||
restart: always
|
||||
volumes:
|
||||
- chatwoot-postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DATABASE}
|
||||
- POSTGRES_USER=${POSTGRES_USERNAME}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
|
||||
chatwoot-redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- chatwoot-redis-data:/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
chatwoot-storage:
|
||||
chatwoot-postgres-data:
|
||||
chatwoot-redis-data:
|
||||
46
apps/dokploy/templates/chatwoot/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generateBase64,
|
||||
generatePassword,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
const secretKeyBase = generateBase64(64);
|
||||
const postgresPassword = generatePassword();
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 3000,
|
||||
serviceName: "chatwoot-rails",
|
||||
},
|
||||
];
|
||||
|
||||
const envs = [
|
||||
`FRONTEND_URL=http://${mainDomain}`,
|
||||
`SECRET_KEY_BASE=${secretKeyBase}`,
|
||||
"RAILS_ENV=production",
|
||||
"NODE_ENV=production",
|
||||
"INSTALLATION_ENV=docker",
|
||||
"RAILS_LOG_TO_STDOUT=true",
|
||||
"LOG_LEVEL=info",
|
||||
"DEFAULT_LOCALE=en",
|
||||
"POSTGRES_HOST=chatwoot-postgres",
|
||||
"POSTGRES_PORT=5432",
|
||||
"POSTGRES_DATABASE=chatwoot",
|
||||
"POSTGRES_USERNAME=postgres",
|
||||
`POSTGRES_PASSWORD=${postgresPassword}`,
|
||||
"REDIS_URL=redis://chatwoot-redis:6379",
|
||||
"ENABLE_ACCOUNT_SIGNUP=false",
|
||||
"ACTIVE_STORAGE_SERVICE=local",
|
||||
];
|
||||
|
||||
return {
|
||||
domains,
|
||||
envs,
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
tickets-postgres:
|
||||
image: mysql:8
|
||||
restart: unless-stopped
|
||||
hostname: mysql
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- tickets-mysql:/var/lib/mysql
|
||||
- tickets-mysql-data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
@@ -20,23 +19,22 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
bot:
|
||||
tickets-app:
|
||||
image: eartharoid/discord-tickets:4.0.21
|
||||
depends_on:
|
||||
mysql:
|
||||
tickets-postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
hostname: bot
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- tickets-bot:/home/container/user
|
||||
- tickets-app-data:/home/container/user
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
tty: true
|
||||
stdin_open: true
|
||||
environment:
|
||||
DB_CONNECTION_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql/${MYSQL_DATABASE}
|
||||
DB_CONNECTION_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@tickets-postgres/${MYSQL_DATABASE}
|
||||
DISCORD_SECRET: ${DISCORD_SECRET}
|
||||
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
@@ -49,6 +47,10 @@ services:
|
||||
PUBLISH_COMMANDS: "true"
|
||||
SUPER: ${SUPER_USERS}
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
tickets-mysql:
|
||||
tickets-bot:
|
||||
tickets-mysql-data:
|
||||
tickets-app-data:
|
||||
@@ -13,7 +13,6 @@ export function generate(schema: Schema): Template {
|
||||
const mysqlUser = "tickets";
|
||||
const mysqlDatabase = "tickets";
|
||||
|
||||
// Generate encryption key in the format they use
|
||||
const encryptionKey = Array.from({ length: 48 }, () =>
|
||||
Math.floor(Math.random() * 16).toString(16),
|
||||
).join("");
|
||||
@@ -22,7 +21,7 @@ export function generate(schema: Schema): Template {
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 8169,
|
||||
serviceName: "bot",
|
||||
serviceName: "tickets-app",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,7 +32,6 @@ export function generate(schema: Schema): Template {
|
||||
`MYSQL_ROOT_PASSWORD=${mysqlRootPassword}`,
|
||||
`MYSQL_USER=${mysqlUser}`,
|
||||
`ENCRYPTION_KEY=${encryptionKey}`,
|
||||
// These need to be set by the user through the UI
|
||||
"# Follow the guide at: https://discordtickets.app/self-hosting/installation/docker/#creating-the-discord-application",
|
||||
"DISCORD_SECRET=",
|
||||
"DISCORD_TOKEN=",
|
||||
|
||||
94
apps/dokploy/templates/discourse/docker-compose.yml
Normal file
@@ -0,0 +1,94 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
discourse-db:
|
||||
image: docker.io/bitnami/postgresql:17
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- discourse-postgresql-data:/bitnami/postgresql
|
||||
environment:
|
||||
POSTGRESQL_USERNAME: bn_discourse
|
||||
POSTGRESQL_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRESQL_DATABASE: bitnami_discourse
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U bn_discourse -d bitnami_discourse"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
discourse-redis:
|
||||
image: docker.io/bitnami/redis:7.4
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- discourse-redis-data:/bitnami/redis
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
discourse-app:
|
||||
image: docker.io/bitnami/discourse:3.3.2
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- discourse-data:/bitnami/discourse
|
||||
depends_on:
|
||||
discourse-db:
|
||||
condition: service_healthy
|
||||
discourse-redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DISCOURSE_HOST: ${DISCOURSE_HOST}
|
||||
DISCOURSE_DATABASE_HOST: discourse-db
|
||||
DISCOURSE_DATABASE_PORT_NUMBER: 5432
|
||||
DISCOURSE_DATABASE_USER: bn_discourse
|
||||
DISCOURSE_DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DISCOURSE_DATABASE_NAME: bitnami_discourse
|
||||
DISCOURSE_REDIS_HOST: discourse-redis
|
||||
DISCOURSE_REDIS_PORT_NUMBER: 6379
|
||||
DISCOURSE_REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
# Optional: Configure SMTP for email delivery
|
||||
# DISCOURSE_SMTP_HOST: ${SMTP_HOST}
|
||||
# DISCOURSE_SMTP_PORT: ${SMTP_PORT}
|
||||
# DISCOURSE_SMTP_USER: ${SMTP_USER}
|
||||
# DISCOURSE_SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
restart: unless-stopped
|
||||
|
||||
discourse-sidekiq:
|
||||
image: docker.io/bitnami/discourse:3.3.2
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- discourse-sidekiq-data:/bitnami/discourse
|
||||
depends_on:
|
||||
- discourse-app
|
||||
command: /opt/bitnami/scripts/discourse-sidekiq/run.sh
|
||||
environment:
|
||||
DISCOURSE_HOST: ${DISCOURSE_HOST}
|
||||
DISCOURSE_DATABASE_HOST: discourse-db
|
||||
DISCOURSE_DATABASE_PORT_NUMBER: 5432
|
||||
DISCOURSE_DATABASE_USER: bn_discourse
|
||||
DISCOURSE_DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DISCOURSE_DATABASE_NAME: bitnami_discourse
|
||||
DISCOURSE_REDIS_HOST: discourse-redis
|
||||
DISCOURSE_REDIS_PORT_NUMBER: 6379
|
||||
DISCOURSE_REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
# Optional: Configure SMTP for email delivery
|
||||
# DISCOURSE_SMTP_HOST: ${SMTP_HOST}
|
||||
# DISCOURSE_SMTP_PORT: ${SMTP_PORT}
|
||||
# DISCOURSE_SMTP_USER: ${SMTP_USER}
|
||||
# DISCOURSE_SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
discourse-postgresql-data:
|
||||
discourse-redis-data:
|
||||
discourse-data:
|
||||
discourse-sidekiq-data:
|
||||
37
apps/dokploy/templates/discourse/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generatePassword,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
const postgresPassword = generatePassword();
|
||||
const redisPassword = generatePassword();
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 3000,
|
||||
serviceName: "discourse-app",
|
||||
},
|
||||
];
|
||||
|
||||
const envs = [
|
||||
`DISCOURSE_HOST=${mainDomain}`,
|
||||
`POSTGRES_PASSWORD=${postgresPassword}`,
|
||||
`REDIS_PASSWORD=${redisPassword}`,
|
||||
"# Optional: Configure SMTP for email delivery",
|
||||
"# SMTP_HOST=smtp.example.com",
|
||||
"# SMTP_PORT=587",
|
||||
"# SMTP_USER=your_smtp_user",
|
||||
"# SMTP_PASSWORD=your_smtp_password",
|
||||
];
|
||||
|
||||
return {
|
||||
domains,
|
||||
envs,
|
||||
};
|
||||
}
|
||||
48
apps/dokploy/templates/heyform/docker-compose.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
services:
|
||||
heyform:
|
||||
image: heyform/community-edition:latest
|
||||
restart: always
|
||||
volumes:
|
||||
# Persist uploaded images
|
||||
- heyform-data:/app/static/upload
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
ports:
|
||||
- 8000
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MONGO_URI: 'mongodb://mongo:27017/heyform'
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
networks:
|
||||
- heyform-network
|
||||
|
||||
mongo:
|
||||
image: percona/percona-server-mongodb:4.4
|
||||
restart: always
|
||||
networks:
|
||||
- heyform-network
|
||||
volumes:
|
||||
# Persist MongoDB data
|
||||
- mongo-data:/data/db
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: "redis-server --appendonly yes"
|
||||
networks:
|
||||
- heyform-network
|
||||
volumes:
|
||||
# Persist KeyDB data
|
||||
- redis-data:/data
|
||||
|
||||
networks:
|
||||
heyform-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
heyform-data:
|
||||
mongo-data:
|
||||
redis-data:
|
||||
32
apps/dokploy/templates/heyform/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generateBase64,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
const sessionKey = generateBase64(64);
|
||||
const formEncryptionKey = generateBase64(64);
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 8000,
|
||||
serviceName: "heyform",
|
||||
},
|
||||
];
|
||||
|
||||
const envs = [
|
||||
`APP_HOMEPAGE_URL=http://${mainDomain}`,
|
||||
`SESSION_KEY=${sessionKey}`,
|
||||
`FORM_ENCRYPTION_KEY=${formEncryptionKey}`,
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
domains,
|
||||
};
|
||||
}
|
||||
111
apps/dokploy/templates/immich/docker-compose.yml
Normal file
@@ -0,0 +1,111 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
image: ghcr.io/immich-app/immich-server:v1.121.0
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- immich-library:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
immich-redis:
|
||||
condition: service_healthy
|
||||
immich-database:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: 2283
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL}
|
||||
# Database Configuration
|
||||
DB_HOSTNAME: ${DB_HOSTNAME}
|
||||
DB_PORT: ${DB_PORT}
|
||||
DB_USERNAME: ${DB_USERNAME}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_DATABASE_NAME: ${DB_DATABASE_NAME}
|
||||
# Redis Configuration
|
||||
REDIS_HOSTNAME: ${REDIS_HOSTNAME}
|
||||
REDIS_PORT: ${REDIS_PORT}
|
||||
REDIS_DBINDEX: ${REDIS_DBINDEX}
|
||||
# Server Configuration
|
||||
TZ: ${TZ}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:2283/server-info/ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
immich-machine-learning:
|
||||
image: ghcr.io/immich-app/immich-machine-learning:v1.121.0
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- immich-model-cache:/cache
|
||||
environment:
|
||||
REDIS_HOSTNAME: ${REDIS_HOSTNAME}
|
||||
REDIS_PORT: ${REDIS_PORT}
|
||||
REDIS_DBINDEX: ${REDIS_DBINDEX}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3003/ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
immich-redis:
|
||||
image: redis:6.2-alpine
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- immich-redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: always
|
||||
|
||||
immich-database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- immich-postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: immich
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
healthcheck:
|
||||
test: pg_isready -U ${DB_USERNAME} -d immich || exit 1
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
command:
|
||||
[
|
||||
'postgres',
|
||||
'-c',
|
||||
'shared_preload_libraries=vectors.so',
|
||||
'-c',
|
||||
'search_path="$$user", public, vectors',
|
||||
'-c',
|
||||
'logging_collector=on',
|
||||
'-c',
|
||||
'max_wal_size=2GB',
|
||||
'-c',
|
||||
'shared_buffers=512MB',
|
||||
'-c',
|
||||
'wal_compression=on',
|
||||
]
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
immich-model-cache:
|
||||
immich-postgres:
|
||||
immich-library:
|
||||
immich-redis-data:
|
||||
46
apps/dokploy/templates/immich/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generateBase64,
|
||||
generatePassword,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
const dbPassword = generatePassword();
|
||||
const dbUser = "immich";
|
||||
const appSecret = generateBase64(32);
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 2283,
|
||||
serviceName: "immich-server",
|
||||
},
|
||||
];
|
||||
|
||||
const envs = [
|
||||
`IMMICH_HOST=${mainDomain}`,
|
||||
`SERVER_URL=https://${mainDomain}`,
|
||||
`FRONT_BASE_URL=https://${mainDomain}`,
|
||||
"# Database Configuration",
|
||||
"DB_HOSTNAME=immich-database",
|
||||
"DB_PORT=5432",
|
||||
`DB_USERNAME=${dbUser}`,
|
||||
`DB_PASSWORD=${dbPassword}`,
|
||||
"DB_DATABASE_NAME=immich",
|
||||
"# Redis Configuration",
|
||||
"REDIS_HOSTNAME=immich-redis",
|
||||
"REDIS_PORT=6379",
|
||||
"REDIS_DBINDEX=0",
|
||||
"# Server Configuration",
|
||||
"TZ=UTC",
|
||||
];
|
||||
|
||||
return {
|
||||
domains,
|
||||
envs,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
invoiceshelf_db:
|
||||
invoiceshelf-postgres:
|
||||
image: postgres:15
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- invoiceshelf-postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_USER=${DB_USERNAME}
|
||||
@@ -17,13 +17,13 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
invoiceshelf:
|
||||
invoiceshelf-app:
|
||||
image: invoiceshelf/invoiceshelf:latest
|
||||
networks:
|
||||
- dokploy-network
|
||||
volumes:
|
||||
- app_data:/data
|
||||
- app_conf:/conf
|
||||
- invoiceshelf-app-data:/data
|
||||
- invoiceshelf-app-conf:/conf
|
||||
environment:
|
||||
- PHP_TZ=UTC
|
||||
- TIMEZONE=UTC
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
- APP_DEBUG=false
|
||||
- APP_URL=http://${INVOICESHELF_HOST}
|
||||
- DB_CONNECTION=pgsql
|
||||
- DB_HOST=invoiceshelf_db
|
||||
- DB_HOST=invoiceshelf-postgres
|
||||
- DB_PORT=5432
|
||||
- DB_DATABASE=${DB_DATABASE}
|
||||
- DB_USERNAME=${DB_USERNAME}
|
||||
@@ -46,10 +46,14 @@ services:
|
||||
- SANCTUM_STATEFUL_DOMAINS=${INVOICESHELF_HOST}
|
||||
- STARTUP_DELAY=10
|
||||
depends_on:
|
||||
invoiceshelf_db:
|
||||
invoiceshelf-postgres:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
app_data:
|
||||
app_conf:
|
||||
invoiceshelf-postgres-data:
|
||||
invoiceshelf-app-data:
|
||||
invoiceshelf-app-conf:
|
||||
@@ -16,7 +16,7 @@ export function generate(schema: Schema): Template {
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 80,
|
||||
serviceName: "invoiceshelf",
|
||||
serviceName: "invoiceshelf-app",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
14
apps/dokploy/templates/ontime/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
ontime:
|
||||
image: getontime/ontime:v3.8.0
|
||||
ports:
|
||||
- 4001
|
||||
- 8888
|
||||
- 9999
|
||||
volumes:
|
||||
- ontime-data:/data/
|
||||
environment:
|
||||
- TZ
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
ontime-data:
|
||||
25
apps/dokploy/templates/ontime/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 4001,
|
||||
serviceName: "ontime",
|
||||
},
|
||||
];
|
||||
|
||||
const envs = ["TZ=UTC"];
|
||||
|
||||
return {
|
||||
domains,
|
||||
envs,
|
||||
};
|
||||
}
|
||||