chore(template): resolve conflicts with main

This commit is contained in:
Tam Nguyen
2025-02-03 11:49:17 +11:00
95 changed files with 15410 additions and 787 deletions

View File

@@ -144,38 +144,6 @@ export const ShowResources = ({ id, type }: Props) => {
className="grid w-full gap-8 "
>
<div className="grid w-full md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
@@ -209,6 +177,37 @@ export const ShowResources = ({ id, type }: Props) => {
);
}}
/>
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}

View File

@@ -45,8 +45,8 @@ export const ShowVolumes = ({ id, type }: Props) => {
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this postgres database use the
following config to setup the volumes
If you want to persist data in this service use the following config
to setup the volumes
</CardDescription>
</div>
@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
{mount.content}
</span>
</div>
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
{mount.type === "file" ? (
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)}
</div>
<div className="flex flex-row gap-1">
<UpdateVolume

View File

@@ -17,8 +17,15 @@ interface Props {
open: boolean;
onClose: () => void;
serverId?: string;
errorMessage?: string;
}
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
export const ShowDeployment = ({
logPath,
open,
onClose,
serverId,
errorMessage,
}: Props) => {
const [data, setData] = useState("");
const [showExtraLogs, setShowExtraLogs] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
@@ -99,6 +106,8 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
}
}, [filteredLogs, autoScroll]);
const optionalErrors = parseLogs(errorMessage || "");
return (
<Dialog
open={open}
@@ -157,9 +166,17 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</>
)}
</div>
</DialogContent>

View File

@@ -8,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues";
@@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment";
interface Props {
applicationId: string;
}
export const ShowDeployments = ({ applicationId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data } = api.application.one.useQuery({ applicationId });
const { data: deployments } = api.deployment.all.useQuery(
{ applicationId },
@@ -100,7 +103,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -112,9 +115,10 @@ export const ShowDeployments = ({ applicationId }: Props) => {
)}
<ShowDeployment
serverId={data?.serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</CardContent>
</Card>

View File

@@ -98,8 +98,12 @@ export const ShowDomains = ({ applicationId }: Props) => {
applicationId={applicationId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</AddDomain>
<DialogAction

View File

@@ -235,7 +235,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -245,7 +245,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -226,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -236,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -248,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -260,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -26,7 +26,9 @@ export const ShowPreviewBuilds = ({
serverId,
trigger,
}: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -77,7 +79,7 @@ export const ShowPreviewBuilds = ({
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -89,9 +91,10 @@ export const ShowPreviewBuilds = ({
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</Dialog>
);

View File

@@ -20,9 +20,10 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import type { ServiceType } from "@dokploy/server/db/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -39,16 +40,42 @@ const deleteComposeSchema = z.object({
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
interface Props {
composeId: string;
id: string;
type: ServiceType | "application";
}
export const DeleteCompose = ({ composeId }: Props) => {
export const DeleteService = ({ id, type }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
const { data } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
postgres: () => api.postgres.remove.useMutation(),
redis: () => api.redis.remove.useMutation(),
mysql: () => api.mysql.remove.useMutation(),
mariadb: () => api.mariadb.remove.useMutation(),
application: () => api.application.delete.useMutation(),
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
const form = useForm<DeleteCompose>({
defaultValues: {
@@ -62,14 +89,23 @@ export const DeleteCompose = ({ composeId }: Props) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
const { deleteVolumes } = formData;
await mutateAsync({ composeId, deleteVolumes })
await mutateAsync({
mongoId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
applicationId: id || "",
composeId: id || "",
deleteVolumes,
})
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
toast.success("Compose deleted successfully");
toast.success("deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the compose");
toast.error("Error deleting the service");
});
} else {
form.setError("projectName", {
@@ -95,8 +131,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
compose. If you are sure please enter the compose name to delete
this compose.
service. If you are sure please enter the service name to delete
this service.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
@@ -119,9 +155,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
variant="outline"
onClick={() => {
if (data?.name && data?.appName) {
navigator.clipboard.writeText(
`${data.name}/${data.appName}`,
);
copy(`${data.name}/${data.appName}`);
toast.success("Copied to clipboard. Be careful!");
}
}}
@@ -142,27 +176,29 @@ export const DeleteCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
{type === "compose" && (
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
</div>

View File

@@ -17,12 +17,14 @@ interface Props {
serverId?: string;
open: boolean;
onClose: () => void;
errorMessage?: string;
}
export const ShowDeploymentCompose = ({
logPath,
open,
onClose,
serverId,
errorMessage,
}: Props) => {
const [data, setData] = useState("");
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
@@ -105,6 +107,8 @@ export const ShowDeploymentCompose = ({
}
}, [filteredLogs, autoScroll]);
const optionalErrors = parseLogs(errorMessage || "");
return (
<Dialog
open={open}
@@ -161,9 +165,17 @@ export const ShowDeploymentCompose = ({
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</>
)}
</div>
</DialogContent>

View File

@@ -8,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueuesCompose } from "./cancel-queues-compose";
@@ -19,7 +19,9 @@ interface Props {
composeId: string;
}
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data } = api.compose.one.useQuery({ composeId });
const { data: deployments } = api.deployment.allByCompose.useQuery(
{ composeId },
@@ -100,7 +102,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -112,9 +114,10 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
)}
<ShowDeploymentCompose
serverId={data?.serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</CardContent>
</Card>

View File

@@ -97,8 +97,12 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
composeId={composeId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</AddDomainCompose>
<DialogAction

View File

@@ -237,7 +237,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -247,7 +247,12 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -228,7 +228,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -238,7 +238,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -250,7 +250,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -262,7 +262,12 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -70,7 +70,7 @@ interface Props {
export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -166,7 +166,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -197,7 +197,12 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -73,6 +73,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
@@ -173,7 +174,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -204,7 +205,12 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -89,7 +89,7 @@ const mySchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("postgres"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("postgres"),
databaseUser: z.string().default("postgres"),
})
.merge(baseDatabaseSchema),
@@ -110,7 +110,7 @@ const mySchema = z.discriminatedUnion("type", [
type: z.literal("mysql"),
databaseRootPassword: z.string().default(""),
databaseUser: z.string().default("mysql"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("mysql"),
})
.merge(baseDatabaseSchema),
z
@@ -119,7 +119,7 @@ const mySchema = z.discriminatedUnion("type", [
dockerImage: z.string().default("mariadb:4"),
databaseRootPassword: z.string().default(""),
databaseUser: z.string().default("mariadb"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("mariadb"),
})
.merge(baseDatabaseSchema),
]);
@@ -206,7 +206,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
promise = postgresMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "postgres",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
@@ -233,7 +233,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
...commonParams,
databasePassword: data.databasePassword,
databaseRootPassword: data.databaseRootPassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "mariadb",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
@@ -242,7 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
promise = mysqlMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "mysql",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,

View File

@@ -80,6 +80,7 @@ export const AddTemplate = ({ projectId }: Props) => {
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data } = api.compose.templates.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: tags, isLoading: isLoadingTags } =
api.compose.getTags.useQuery();
@@ -226,7 +227,11 @@ export const AddTemplate = ({ projectId }: Props) => {
<ScrollArea className="h-[calc(98vh-8rem)]">
<div className="p-6">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isError && (
<AlertBlock type="error" className="mb-4">
{error?.message}
</AlertBlock>
)}
{templates.length === 0 ? (
<div className="flex justify-center items-center w-full gap-2 min-h-[50vh]">
@@ -304,7 +309,7 @@ export const AddTemplate = ({ projectId }: Props) => {
{/* Create Button */}
<div
className={cn(
"flex-none px-6 pb-6 pt-3 mt-auto",
"flex-none px-6 py-3 mt-auto",
viewMode === "detailed"
? "flex items-center justify-between bg-muted/30 border-t"
: "flex justify-center",
@@ -368,7 +373,8 @@ export const AddTemplate = ({ projectId }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server (Optional)
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
@@ -401,7 +407,12 @@ export const AddTemplate = ({ projectId }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>

View File

@@ -118,7 +118,7 @@ export const HandleProject = ({ projectId }: Props) => {
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a project</DialogTitle>
<DialogTitle>{projectId ? "Update" : "Add a"} project</DialogTitle>
<DialogDescription>The home of something big!</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}

View File

@@ -23,8 +23,10 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
@@ -87,9 +89,12 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
<div className="">
<HandleProject />
</div>
{(auth?.rol === "admin" || user?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
)}
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
@@ -146,14 +151,91 @@ export const ShowProjects = () => {
href={`/dashboard/project/${project.projectId}`}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
{project.applications.length > 0 ||
project.compose.length > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.applications.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Applications
</DropdownMenuLabel>
{project.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
{app.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
</DropdownMenuGroup>
)}
{project.compose.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Compose
</DropdownMenuLabel>
{project.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
{comp.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5">
@@ -179,7 +261,10 @@ export const ShowProjects = () => {
<MoreHorizontalIcon className="size-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px] space-y-2">
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuLabel className="font-normal">
Actions
</DropdownMenuLabel>

View File

@@ -61,6 +61,7 @@ export const AddCertificate = () => {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -181,7 +182,7 @@ export const AddCertificate = () => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud && "(Optional)"}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -202,7 +203,12 @@ export const AddCertificate = () => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { sshKeyCreate, type sshKeyType } from "@/server/db/validations";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { DownloadIcon, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -111,6 +111,26 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
toast.error("Error generating the SSH Key");
});
const downloadKey = (
content: string,
defaultFilename: string,
keyType: "private" | "public",
) => {
const keyName = form.watch("name");
const filename = keyName
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
: `${keyType}_${defaultFilename}`;
const blob = new Blob([content], { type: "text/plain" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
@@ -245,7 +265,41 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
</FormItem>
)}
/>
<DialogFooter>
<DialogFooter className="flex items-center justify-between">
<div className="flex items-center gap-4">
{form.watch("privateKey") && (
<Button
type="button"
variant="outline"
size="default"
onClick={() =>
downloadKey(form.watch("privateKey"), "id_rsa", "private")
}
className="flex items-center gap-2"
>
<DownloadIcon className="h-4 w-4" />
Private Key
</Button>
)}
{form.watch("publicKey") && (
<Button
type="button"
variant="outline"
size="default"
onClick={() =>
downloadKey(
form.watch("publicKey"),
"id_rsa.pub",
"public",
)
}
className="flex items-center gap-2"
>
<DownloadIcon className="h-4 w-4" />
Public Key
</Button>
)}
</div>
<Button isLoading={isLoading} type="submit">
{sshKeyId ? "Update" : "Create"}
</Button>

View File

@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css";
import { AttachAddon } from "@xterm/addon-attach";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { useTheme } from "next-themes";
import { getLocalServerData } from "./local-server-config";
@@ -37,6 +38,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
foreground: "currentColor",
},
});
const addonFit = new FitAddon();
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
@@ -54,6 +56,8 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
const ws = new WebSocket(wsUrl);
const addonAttach = new AttachAddon(ws);
const clipboardAddon = new ClipboardAddon();
term.loadAddon(clipboardAddon);
// @ts-ignore
term.open(termRef.current);
@@ -68,7 +72,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
return (
<div className="flex flex-col gap-4">
<div className="w-full h-full bg-transparent border rounded-lg p-2 ">
<div className="w-full h-full bg-transparent border rounded-lg p-2">
<div id={id} ref={termRef} className="rounded-xl" />
</div>
</div>

View File

@@ -1,7 +1,6 @@
"use client";
import {
Activity,
AudioWaveform,
BarChartHorizontalBigIcon,
Bell,
BlocksIcon,
@@ -9,7 +8,6 @@ import {
Boxes,
ChevronRight,
CircleHelp,
Command,
CreditCard,
Database,
Folder,
@@ -27,8 +25,8 @@ import {
Users,
} from "lucide-react";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import type * as React from "react";
import { useEffect, useState } from "react";
import {
Breadcrumb,
@@ -65,243 +63,290 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import type { AppRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { useRouter } from "next/router";
import { Logo } from "../shared/logo";
import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav";
// This is sample data.
interface NavItem {
// The types of the queries we are going to use
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
type SingleNavItem = {
isSingle?: true;
title: string;
url: string;
icon: LucideIcon;
isSingle: boolean;
isActive: boolean;
items?: {
title: string;
url: string;
icon?: LucideIcon;
}[];
}
icon?: LucideIcon;
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
interface ExternalLink {
// NavItem type
// Consists of a single item or a group of items
// If `isSingle` is true or undefined, the item is a single item
// If `isSingle` is false, the item is a group of items
type NavItem =
| SingleNavItem
| {
isSingle: false;
title: string;
icon: LucideIcon;
items: SingleNavItem[];
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
// ExternalLink type
// Represents an external link item (used for the help section)
type ExternalLink = {
name: string;
url: string;
icon: React.ComponentType<{ className?: string }>;
}
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Dokploy",
logo: Logo,
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
],
// Menu type
// Consists of home, settings, and help items
type Menu = {
home: NavItem[];
settings: NavItem[];
help: ExternalLink[];
};
// Menu items
// Consists of unfiltered home, settings, and help items
// The items are filtered based on the user's role and permissions
// The `isEnabled` function is called to determine if the item should be displayed
const MENU: Menu = {
home: [
{
isSingle: true,
title: "Projects",
url: "/dashboard/projects",
icon: Folder,
isSingle: true,
isActive: false,
},
{
isSingle: true,
title: "Monitoring",
url: "/dashboard/monitoring",
icon: BarChartHorizontalBigIcon,
isSingle: true,
isActive: false,
// Only enabled in non-cloud environments
isEnabled: ({ auth, user, isCloud }) => !isCloud,
},
{
isSingle: true,
title: "Traefik File System",
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Traefik files in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
!isCloud
),
},
{
isSingle: true,
title: "Docker",
url: "/dashboard/docker",
icon: BlocksIcon,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
{
isSingle: true,
title: "Swarm",
url: "/dashboard/swarm",
icon: PieChart,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
{
isSingle: true,
title: "Requests",
url: "/dashboard/requests",
icon: Forward,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
// Legacy unused menu, adjusted to the new structure
// {
// isSingle: true,
// title: "Projects",
// url: "/dashboard/projects",
// icon: Folder,
// isSingle: true,
// },
// {
// isSingle: true,
// title: "Monitoring",
// icon: BarChartHorizontalBigIcon,
// url: "/dashboard/settings/monitoring",
// isSingle: true,
// },
// {
// title: "Settings",
// url: "#",
// icon: Settings2,
// isActive: true,
// items: [
// {
// title: "Profile",
// url: "/dashboard/settings/profile",
// },
// {
// title: "Users",
// url: "/dashboard/settings/users",
// },
// {
// title: "SSH Key",
// url: "/dashboard/settings/ssh-keys",
// },
// {
// title: "Git",
// url: "/dashboard/settings/git-providers",
// },
// ],
// isSingle: false,
// title: "Settings",
// icon: Settings2,
// items: [
// {
// title: "Profile",
// url: "/dashboard/settings/profile",
// },
// {
// title: "Users",
// url: "/dashboard/settings/users",
// },
// {
// title: "SSH Key",
// url: "/dashboard/settings/ssh-keys",
// },
// {
// title: "Git",
// url: "/dashboard/settings/git-providers",
// },
// ],
// },
// {
// title: "Integrations",
// icon: BlocksIcon,
// items: [
// {
// title: "S3 Destinations",
// url: "/dashboard/settings/destinations",
// },
// {
// title: "Registry",
// url: "/dashboard/settings/registry",
// },
// {
// title: "Notifications",
// url: "/dashboard/settings/notifications",
// },
// ],
] as NavItem[],
// isSingle: false,
// title: "Integrations",
// icon: BlocksIcon,
// items: [
// {
// title: "S3 Destinations",
// url: "/dashboard/settings/destinations",
// },
// {
// title: "Registry",
// url: "/dashboard/settings/registry",
// },
// {
// title: "Notifications",
// url: "/dashboard/settings/notifications",
// },
// ],
// },
],
settings: [
{
title: "Server",
isSingle: true,
title: "Web Server",
url: "/dashboard/settings/server",
icon: Activity,
isSingle: true,
isActive: false,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
},
{
isSingle: true,
title: "Profile",
url: "/dashboard/settings/profile",
icon: User,
isSingle: true,
isActive: false,
},
{
title: "Servers",
isSingle: true,
title: "Remote Servers",
url: "/dashboard/settings/servers",
icon: Server,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Users",
icon: Users,
url: "/dashboard/settings/users",
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "SSH Keys",
icon: KeyRound,
url: "/dashboard/settings/ssh-keys",
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to SSH keys
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
},
{
isSingle: true,
title: "Git",
url: "/dashboard/settings/git-providers",
icon: GitBranch,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Git providers
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
},
{
isSingle: true,
title: "Registry",
url: "/dashboard/settings/registry",
icon: Package,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "S3 Destinations",
url: "/dashboard/settings/destinations",
icon: Database,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Certificates",
url: "/dashboard/settings/certificates",
icon: ShieldCheck,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Cluster",
url: "/dashboard/settings/cluster",
icon: Boxes,
isSingle: true,
isActive: false,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
},
{
isSingle: true,
title: "Notifications",
url: "/dashboard/settings/notifications",
icon: Bell,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Billing",
url: "/dashboard/settings/billing",
icon: CreditCard,
isSingle: true,
isActive: false,
// Only enabled for admins in cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && isCloud),
},
] as NavItem[],
],
help: [
{
name: "Documentation",
@@ -325,8 +370,108 @@ const data = {
/>
),
},
] as ExternalLink[],
};
],
} as const;
/**
* Creates a menu based on the current user's role and permissions
* @returns a menu object with the home, settings, and help items
*/
function createMenuForAuthUser(opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}): Menu {
return {
// Filter the home items based on the user's role and permissions
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
home: MENU.home.filter((item) =>
!item.isEnabled
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
// Filter the settings items based on the user's role and permissions
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
settings: MENU.settings.filter((item) =>
!item.isEnabled
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
// Filter the help items based on the user's role and permissions
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
help: MENU.help.filter((item) =>
!item.isEnabled
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
};
}
/**
* Determines if an item url is active based on the current pathname
* @returns true if the item url is active, false otherwise
*/
function isActiveRoute(opts: {
/** The url of the item. Usually obtained from `item.url` */
itemUrl: string;
/** The current pathname. Usually obtained from `usePathname()` */
pathname: string;
}): boolean {
const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project");
const normalizedPathname = opts.pathname?.replace("/projects", "/project");
if (!normalizedPathname) return false;
if (normalizedPathname === normalizedItemUrl) return true;
if (normalizedPathname.startsWith(normalizedItemUrl)) {
const nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
return nextChar === "/";
}
return false;
}
/**
* Finds the active nav item based on the current pathname
* @returns the active nav item with `SingleNavItem` type or undefined if none is active
*/
function findActiveNavItem(
navItems: NavItem[],
pathname: string,
): SingleNavItem | undefined {
const found = navItems.find((item) =>
item.isSingle !== false
? // The current item is single, so check if the item url is active
isActiveRoute({ itemUrl: item.url, pathname })
: // The current item is not single, so check if any of the sub items are active
item.items.some((item) =>
isActiveRoute({ itemUrl: item.url, pathname }),
),
);
if (found?.isSingle !== false) {
// The found item is single, so return it
return found;
}
// The found item is not single, so find the active sub item
return found?.items.find((item) =>
isActiveRoute({ itemUrl: item.url, pathname }),
);
}
interface Props {
children: React.ReactNode;
@@ -398,64 +543,21 @@ export default function Page({ children }: Props) {
const includesProjects = pathname?.includes("/dashboard/project");
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
const isActiveRoute = (itemUrl: string) => {
const normalizedItemUrl = itemUrl?.replace("/projects", "/project");
const normalizedPathname = pathname?.replace("/projects", "/project");
if (!normalizedPathname) return false;
const {
home: filteredHome,
settings: filteredSettings,
help,
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
if (normalizedPathname === normalizedItemUrl) return true;
const activeItem = findActiveNavItem(
[...filteredHome, ...filteredSettings],
pathname,
);
if (normalizedPathname.startsWith(normalizedItemUrl)) {
const nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
return nextChar === "/";
}
return false;
};
let filteredHome = isCloud
? data.home.filter(
(item) =>
![
"/dashboard/monitoring",
"/dashboard/traefik",
"/dashboard/docker",
"/dashboard/swarm",
"/dashboard/requests",
].includes(item.url),
)
: data.home;
let filteredSettings = isCloud
? data.settings.filter(
(item) =>
![
"/dashboard/settings/server",
"/dashboard/settings/cluster",
].includes(item.url),
)
: data.settings.filter(
(item) => !["/dashboard/settings/billing"].includes(item.url),
);
filteredHome = filteredHome.map((item) => ({
...item,
isActive: isActiveRoute(item.url),
}));
filteredSettings = filteredSettings.map((item) => ({
...item,
isActive: isActiveRoute(item.url),
}));
const activeItem =
filteredHome.find((item) => item.isActive) ||
filteredSettings.find((item) => item.isActive);
const showProjectsButton =
currentPath === "/dashboard/projects" &&
(auth?.rol === "admin" || user?.canCreateProjects);
// const showProjectsButton =
// currentPath === "/dashboard/projects" &&
// (auth?.rol === "admin" || user?.canCreateProjects);
return (
<SidebarProvider
@@ -486,173 +588,185 @@ export default function Page({ children }: Props) {
<SidebarGroup>
<SidebarGroupLabel>Home</SidebarGroupLabel>
<SidebarMenu>
{filteredHome.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
{item.isSingle ? (
<SidebarMenuButton
asChild
tooltip={item.title}
className={cn(isActiveRoute(item.url) && "bg-border")}
>
<Link
href={item.url}
className="flex w-full items-center gap-2"
>
<item.icon
className={cn(
isActiveRoute(item.url) && "text-primary",
)}
/>
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
) : (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={item.isActive}
>
{item.icon && <item.icon />}
{filteredHome.map((item) => {
const isSingle = item.isSingle !== false;
const isActive = isSingle
? isActiveRoute({ itemUrl: item.url, pathname })
: item.items.some((item) =>
isActiveRoute({ itemUrl: item.url, pathname }),
);
<span>{item.title}</span>
{item.items?.length && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
return (
<Collapsible
key={item.title}
asChild
defaultOpen={isActive}
className="group/collapsible"
>
<SidebarMenuItem>
{isSingle ? (
<SidebarMenuButton
asChild
tooltip={item.title}
className={cn(isActive && "bg-border")}
>
<Link
href={item.url}
className="flex w-full items-center gap-2"
>
{item.icon && (
<item.icon
className={cn(isActive && "text-primary")}
/>
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
className={cn(
isActiveRoute(subItem.url) && "bg-border",
)}
>
<Link
href={subItem.url}
className="flex w-full items-center"
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
) : (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={isActive}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.items?.length && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
className={cn(isActive && "bg-border")}
>
{subItem.icon && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActiveRoute(subItem.url) &&
"text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
))}
<Link
href={subItem.url}
className="flex w-full items-center"
>
{subItem.icon && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActive && "text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
);
})}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarMenu className="gap-2">
{filteredSettings.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
{item.isSingle ? (
<SidebarMenuButton
asChild
tooltip={item.title}
className={cn(isActiveRoute(item.url) && "bg-border")}
>
<Link
href={item.url}
className="flex w-full items-center gap-2"
>
<item.icon
className={cn(
isActiveRoute(item.url) && "text-primary",
)}
/>
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
) : (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={item.isActive}
>
{item.icon && <item.icon />}
{filteredSettings.map((item) => {
const isSingle = item.isSingle !== false;
const isActive = isSingle
? isActiveRoute({ itemUrl: item.url, pathname })
: item.items.some((item) =>
isActiveRoute({ itemUrl: item.url, pathname }),
);
<span>{item.title}</span>
{item.items?.length && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
return (
<Collapsible
key={item.title}
asChild
defaultOpen={isActive}
className="group/collapsible"
>
<SidebarMenuItem>
{isSingle ? (
<SidebarMenuButton
asChild
tooltip={item.title}
className={cn(isActive && "bg-border")}
>
<Link
href={item.url}
className="flex w-full items-center gap-2"
>
{item.icon && (
<item.icon
className={cn(isActive && "text-primary")}
/>
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
className={cn(
isActiveRoute(subItem.url) && "bg-border",
)}
>
<Link
href={subItem.url}
className="flex w-full items-center"
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
) : (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={isActive}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.items?.length && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
className={cn(isActive && "bg-border")}
>
{subItem.icon && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActiveRoute(subItem.url) &&
"text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
))}
<Link
href={subItem.url}
className="flex w-full items-center"
>
{subItem.icon && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActive && "text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
);
})}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Extra</SidebarGroupLabel>
<SidebarMenu>
{data.help.map((item: ExternalLink) => (
{help.map((item: ExternalLink) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a
@@ -669,7 +783,7 @@ export default function Page({ children }: Props) {
</SidebarMenuButton>
</SidebarMenuItem>
))}
{!isCloud && (
{!isCloud && auth?.rol === "admin" && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<UpdateServerButton />

View File

@@ -0,0 +1 @@
ALTER TABLE "deployment" ADD COLUMN "errorMessage" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" ALTER COLUMN "cleanupCacheApplications" SET DEFAULT false;

View File

@@ -0,0 +1,3 @@
-- Custom SQL migration file, put you code below!
UPDATE "admin" SET "cleanupCacheApplications" = false;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -407,6 +407,27 @@
"when": 1737306063563,
"tag": "0057_tricky_living_tribunal",
"breakpoints": true
},
{
"idx": 58,
"version": "6",
"when": 1737612903012,
"tag": "0058_brown_sharon_carter",
"breakpoints": true
},
{
"idx": 59,
"version": "6",
"when": 1737615160768,
"tag": "0059_striped_bill_hollister",
"breakpoints": true
},
{
"idx": 60,
"version": "6",
"when": 1737929896838,
"tag": "0060_disable-aggressive-cache",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,7 @@
export const Languages = {
english: { code: "en", name: "English" },
polish: { code: "pl", name: "Polski" },
ukrainian: { code: "uk", name: "Українська" },
russian: { code: "ru", name: "Русский" },
french: { code: "fr", name: "Français" },
german: { code: "de", name: "Deutsch" },
@@ -17,6 +18,7 @@ export const Languages = {
norwegian: { code: "no", name: "Norsk" },
azerbaijani: { code: "az", name: "Azərbaycan" },
indonesian: { code: "id", name: "Bahasa Indonesia" },
malayalam: { code: "ml", name: "മലയാളം" },
};
export type Language = keyof typeof Languages;

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.17.5",
"version": "v0.17.9",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -35,6 +35,18 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"bl": "6.0.11",
"rotating-file-stream": "3.2.3",
"qrcode": "^1.5.3",
"otpauth": "^9.2.3",
"hi-base32": "^0.5.1",
"boxen": "^7.1.1",
"@octokit/auth-app": "^6.0.4",
"nodemailer": "6.9.14",
"@react-email/components": "^0.0.21",
"node-os-utils": "1.3.7",
"@lucia-auth/adapter-drizzle": "1.0.7",
"dockerode": "4.0.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
@@ -75,6 +87,7 @@
"@uiw/react-codemirror": "^4.22.1",
"@xterm/addon-attach": "0.10.0",
"@xterm/xterm": "^5.4.0",
"@xterm/addon-clipboard": "0.1.0",
"adm-zip": "^0.5.14",
"bcrypt": "5.1.1",
"bullmq": "5.4.2",
@@ -127,6 +140,9 @@
"@faker-js/faker": "^8.4.1"
},
"devDependencies": {
"@types/qrcode": "^1.5.5",
"@types/nodemailer": "^6.4.15",
"@types/node-os-utils": "1.3.4",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/js-cookie": "^3.0.6",

View File

@@ -121,7 +121,7 @@ export default async function handler(
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
"deployments",
@@ -156,7 +156,7 @@ export default async function handler(
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
@@ -182,8 +182,9 @@ export default async function handler(
}
} else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id;
const action = githubBody?.action;
if (githubBody?.action === "closed") {
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
@@ -201,79 +202,86 @@ export default async function handler(
res.status(200).json({ message: "Preview Deployment Closed" });
return;
}
// opened or synchronize or reopened
const repository = githubBody?.repository?.name;
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const repository = githubBody?.repository?.name;
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
);
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(400).json({ message: "No Actions matched" });

View File

@@ -13,6 +13,7 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
@@ -82,8 +83,6 @@ const Service = (
},
);
const { mutateAsync, isLoading: isRemoving } =
api.application.delete.useMutation();
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
@@ -177,34 +176,7 @@ const Service = (
<div className="flex flex-row gap-2 justify-end">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Delete Application"
description="Are you sure you want to delete this application?"
type="destructive"
onClick={async () => {
await mutateAsync({
applicationId: applicationId,
})
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Application deleted successfully");
})
.catch(() => {
toast.error("Error deleting application");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={applicationId} type="application" />
)}
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
@@ -168,7 +168,7 @@ const Service = (
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
<DeleteService id={composeId} type="compose" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-external-mariadb-credentials";
import { ShowGeneralMariadb } from "@/components/dashboard/mariadb/general/show-general-mariadb";
@@ -67,8 +68,7 @@ const Mariadb = (
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mariadb.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -148,35 +148,10 @@ const Mariadb = (
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2">
<div className="flex flex-row gap-2 justify-end">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Mariadb"
description="Are you sure you want to delete this mariadb?"
type="destructive"
onClick={async () => {
await remove({ mariadbId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mariadb deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mariadb");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={mariadbId} type="mariadb" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMongoCredentials } from "@/components/dashboard/mongo/general/show-external-mongo-credentials";
import { ShowGeneralMongo } from "@/components/dashboard/mongo/general/show-general-mongo";
@@ -69,8 +70,6 @@ const Mongo = (
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mongo.remove.useMutation();
return (
<div className="pb-10">
@@ -155,32 +154,7 @@ const Mongo = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove mongo"
description="Are you sure you want to delete this mongo?"
type="destructive"
onClick={async () => {
await remove({ mongoId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mongo deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mongo");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={mongoId} type="mongo" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ShowExternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-external-mysql-credentials";
@@ -68,8 +69,6 @@ const MySql = (
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mysql.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -154,32 +153,7 @@ const MySql = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Mysql"
description="Are you sure you want to delete this mysql?"
type="destructive"
onClick={async () => {
await remove({ mysqlId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mysql deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mysql");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={mysqlId} type="mysql" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
@@ -70,9 +71,6 @@ const Postgresql = (
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.postgres.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -156,32 +154,7 @@ const Postgresql = (
<div className="flex flex-row gap-2 justify-end">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Postgres"
description="Are you sure you want to delete this postgres?"
type="destructive"
onClick={async () => {
await remove({ postgresId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Postgres deleted successfully");
})
.catch(() => {
toast.error("Error deleting the postgres");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={postgresId} type="postgres" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials";
@@ -68,8 +69,6 @@ const Redis = (
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.redis.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -153,32 +152,7 @@ const Redis = (
<div className="flex flex-row gap-2 justify-end">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Redis"
description="Are you sure you want to delete this redis?"
type="destructive"
onClick={async () => {
await remove({ redisId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Redis deleted successfully");
})
.catch(() => {
toast.error("Error deleting the redis");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={redisId} type="redis" />
)}
</div>
</div>

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,58 @@
{
"settings.common.save": "സേവ് ചെയ്യുക",
"settings.common.enterTerminal": "ടർമിനലിൽ പ്രവേശിക്കുക",
"settings.server.domain.title": "സർവർ ഡോമെയ്ൻ",
"settings.server.domain.description": "നിങ്ങളുടെ സർവർ അപ്ലിക്കേഷനിൽ ഒരു ഡോമെയ്ൻ ചേർക്കുക.",
"settings.server.domain.form.domain": "ഡോമെയ്ൻ",
"settings.server.domain.form.letsEncryptEmail": "ലെറ്റ്സ് എൻക്രിപ്റ്റ് ഇമെയിൽ",
"settings.server.domain.form.certificate.label": "സർട്ടിഫിക്കറ്റ് പ്രൊവൈഡർ",
"settings.server.domain.form.certificate.placeholder": "ഒരു സർട്ടിഫിക്കറ്റ് തിരഞ്ഞെടുക്കുക",
"settings.server.domain.form.certificateOptions.none": "ഒന്നുമില്ല",
"settings.server.domain.form.certificateOptions.letsencrypt": "ലെറ്റ്സ് എൻക്രിപ്റ്റ്",
"settings.server.webServer.title": "വെബ് സർവർ",
"settings.server.webServer.description": "വെബ് സർവർ റീലോഡ് ചെയ്യുക അല്ലെങ്കിൽ ശുചീകരിക്കുക.",
"settings.server.webServer.actions": "നടപടികൾ",
"settings.server.webServer.reload": "റീലോഡ് ചെയ്യുക",
"settings.server.webServer.watchLogs": "ലോഗുകൾ കാണുക",
"settings.server.webServer.updateServerIp": "സർവർ IP അപ്ഡേറ്റ് ചെയ്യുക",
"settings.server.webServer.server.label": "സർവർ",
"settings.server.webServer.traefik.label": "ട്രാഫിക്",
"settings.server.webServer.traefik.modifyEnv": "ചുറ്റുപാടുകൾ മാറ്റുക",
"settings.server.webServer.traefik.managePorts": "അധിക പോർട്ട് മാപ്പിംഗ്",
"settings.server.webServer.traefik.managePortsDescription": "ട്രാഫിക്കിനായി അധിക പോർട്ടുകൾ ചേർക്കുക അല്ലെങ്കിൽ നീക്കം ചെയ്യുക",
"settings.server.webServer.traefik.targetPort": "ടാർഗറ്റ് പോർട്ട്",
"settings.server.webServer.traefik.publishedPort": "പ്രസിദ്ധീകരിച്ച പോർട്ട്",
"settings.server.webServer.traefik.addPort": "പോർട്ട് ചേർക്കുക",
"settings.server.webServer.traefik.portsUpdated": "പോർട്ടുകൾ വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",
"settings.server.webServer.traefik.portsUpdateError": "പോർട്ടുകൾ അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു",
"settings.server.webServer.traefik.publishMode": "പ്രസിദ്ധീകരണ മോഡ്",
"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": "ഡോക്കർ ബിൽഡറും സിസ്റ്റവും ശുചീകരിക്കുക",
"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": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക",
"settings.terminal.connectionSettings": "കണക്ഷൻ ക്രമീകരണങ്ങൾ",
"settings.terminal.ipAddress": "IP വിലാസം",
"settings.terminal.port": "പോർട്ട്",
"settings.terminal.username": "ഉപയോക്തൃനാമം"
}

View File

@@ -1,5 +1,6 @@
{
"settings.common.save": "Сохранить",
"settings.common.enterTerminal": "Открыть терминал",
"settings.server.domain.title": "Домен сервера",
"settings.server.domain.description": "Установите домен для вашего серверного приложения Dokploy.",
"settings.server.domain.form.domain": "Домен",
@@ -7,18 +8,26 @@
"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.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.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Изменить переменные окружения",
"settings.server.webServer.traefik.managePorts": "Назначение портов",
"settings.server.webServer.traefik.managePortsDescription": "Добавить или удалить дополнительные порты для Traefik",
"settings.server.webServer.traefik.targetPort": "Внутренний порт",
"settings.server.webServer.traefik.publishedPort": "Внешний порт",
"settings.server.webServer.traefik.addPort": "Добавить порт",
"settings.server.webServer.traefik.portsUpdated": "Порты успешно обновлены",
"settings.server.webServer.traefik.portsUpdateError": "Не удалось обновить порты",
"settings.server.webServer.traefik.publishMode": "Режим сопоставления",
"settings.server.webServer.storage.label": "Дисковое пространство",
"settings.server.webServer.storage.cleanUnusedImages": "Очистить неиспользуемые образы",
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистить неиспользуемые тома",
"settings.server.webServer.storage.cleanStoppedContainers": "Очистить остановленные контейнеры",
@@ -40,5 +49,10 @@
"settings.appearance.themes.dark": "Темная",
"settings.appearance.themes.system": "Системная",
"settings.appearance.language": "Язык",
"settings.appearance.languageDescription": "Select a language for your dashboard"
"settings.appearance.languageDescription": "Выберите язык для панели управления",
"settings.terminal.connectionSettings": "Настройки подключения",
"settings.terminal.ipAddress": "IP адрес",
"settings.terminal.port": "Порт",
"settings.terminal.username": "Имя пользователя"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,58 @@
{
"settings.common.save": "Зберегти",
"settings.common.enterTerminal": "Увійти в термінал",
"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": "Оновити IP-адресу сервера",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Змінити середовище",
"settings.server.webServer.traefik.managePorts": "Додаткові порти",
"settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik",
"settings.server.webServer.traefik.targetPort": "Цільовий порт",
"settings.server.webServer.traefik.publishedPort": "Опублікований порт",
"settings.server.webServer.traefik.addPort": "Додати порт",
"settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено",
"settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти",
"settings.server.webServer.traefik.publishMode": "Режим публікації",
"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": "Оберіть мову для вашої панелі керування",
"settings.terminal.connectionSettings": "Налаштування з'єднання",
"settings.terminal.ipAddress": "IP-адреса",
"settings.terminal.port": "Порт",
"settings.terminal.username": "Ім'я користувача"
}

View File

@@ -0,0 +1,10 @@
<svg width="1252" height="1252" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<g id="#70c6beff">
<path id="svg_2" d="m634.37,138.38c11.88,-1.36 24.25,1.3 34.18,8.09c14.96,9.66 25.55,24.41 34.49,39.51c40.59,68.03 81.45,135.91 122.02,203.96c54.02,90.99 108.06,181.97 161.94,273.06c37.28,63 74.65,125.96 112.18,188.82c24.72,41.99 50.21,83.54 73.84,126.16c10.18,17.84 15.77,38.44 14.93,59.03c-0.59,15.92 -3.48,32.28 -11.84,46.08c-11.73,19.46 -31.39,33.2 -52.71,40.36c-11.37,4.09 -23.3,6.87 -35.43,6.89c-132.32,-0.05 -264.64,0.04 -396.95,0.03c-11.38,-0.29 -22.95,-1.6 -33.63,-5.72c-7.81,-3.33 -15.5,-7.43 -21.61,-13.42c-10.43,-10.32 -17.19,-24.96 -15.38,-39.83c0.94,-10.39 3.48,-20.64 7.76,-30.16c4.15,-9.77 9.99,-18.67 15.06,-27.97c22.13,-39.47 45.31,-78.35 69.42,-116.65c7.72,-12.05 14.44,-25.07 25.12,-34.87c11.35,-10.39 25.6,-18.54 41.21,-19.6c12.55,-0.52 24.89,3.82 35.35,10.55c11.8,6.92 21.09,18.44 24.2,31.88c4.49,17.01 -0.34,34.88 -7.55,50.42c-8.09,17.65 -19.62,33.67 -25.81,52.18c-1.13,4.21 -2.66,9.52 0.48,13.23c3.19,3 7.62,4.18 11.77,5.22c12,2.67 24.38,1.98 36.59,2.06c45,-0.01 90,0 135,0c8.91,-0.15 17.83,0.3 26.74,-0.22c6.43,-0.74 13.44,-1.79 18.44,-6.28c3.3,-2.92 3.71,-7.85 2.46,-11.85c-2.74,-8.86 -7.46,-16.93 -12.12,-24.89c-119.99,-204.91 -239.31,-410.22 -360.56,-614.4c-3.96,-6.56 -7.36,-13.68 -13.03,-18.98c-2.8,-2.69 -6.95,-4.22 -10.77,-3.11c-3.25,1.17 -5.45,4.03 -7.61,6.57c-5.34,6.81 -10.12,14.06 -14.51,21.52c-20.89,33.95 -40.88,68.44 -61.35,102.64c-117.9,198.43 -235.82,396.85 -353.71,595.29c-7.31,13.46 -15.09,26.67 -23.57,39.43c-7.45,10.96 -16.49,21.23 -28.14,27.83c-13.73,7.94 -30.69,11.09 -46.08,6.54c-11.23,-3.47 -22.09,-9.12 -30.13,-17.84c-10.18,-10.08 -14.69,-24.83 -14.17,-38.94c0.52,-14.86 5.49,-29.34 12.98,-42.1c71.58,-121.59 143.62,-242.92 215.93,-364.09c37.2,-62.8 74.23,-125.69 111.64,-188.36c37.84,-63.5 75.77,-126.94 113.44,-190.54c21.02,-35.82 42.19,-71.56 64.28,-106.74c6.79,-11.15 15.58,-21.15 26.16,-28.85c8.68,-5.92 18.42,-11 29.05,-11.94z" fill="#70c6be"/>
</g>
<g id="#1ba0d8ff">
<path id="svg_3" d="m628.35,608.38c17.83,-2.87 36.72,1.39 51.5,11.78c11.22,8.66 19.01,21.64 21.26,35.65c1.53,10.68 0.49,21.75 -3.44,31.84c-3.02,8.73 -7.35,16.94 -12.17,24.81c-68.76,115.58 -137.5,231.17 -206.27,346.75c-8.8,14.47 -16.82,29.47 -26.96,43.07c-7.37,9.11 -16.58,16.85 -27.21,21.89c-22.47,11.97 -51.79,4.67 -68.88,-13.33c-8.66,-8.69 -13.74,-20.63 -14.4,-32.84c-0.98,-12.64 1.81,-25.42 7.53,-36.69c5.03,-10.96 10.98,-21.45 17.19,-31.77c30.22,-50.84 60.17,-101.84 90.3,-152.73c41.24,-69.98 83.16,-139.55 124.66,-209.37c4.41,-7.94 9.91,-15.26 16.09,-21.9c8.33,-8.46 18.9,-15.3 30.8,-17.16z" fill="#1ba0d8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,5 @@
<svg width="101" height="100" viewBox="0 0 101 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69.3721 38.0715H61.6579V30.3573C61.6579 25.4287 57.6579 21.4287 52.7293 21.4287H46.7649C41.8363 21.4287 37.8363 25.4287 37.8363 30.3573V38.0715H22.9792V45.2143H44.9792V28.5716H54.515V45.2143H71.1578V54.75H61.6221H54.515V71.393H44.9792V54.75H22.9792V61.8928H37.8363V69.6072C37.8363 74.5358 41.8363 78.5358 46.7649 78.5358H52.7293C57.6579 78.5358 61.6579 74.5358 61.6579 69.6072V61.8928H69.3721C74.3006 61.8928 78.3006 57.8928 78.3006 52.9643V47C78.3006 42.0715 74.3006 38.0715 69.3721 38.0715Z" fill="white"/>
<path d="M72.0506 0H29.1935C13.4139 0 0.62207 12.7919 0.62207 28.5714V71.4286C0.62207 87.2081 13.4139 100 29.1935 100H72.0506C87.8302 100 100.622 87.2081 100.622 71.4286V28.5714C100.622 12.7919 87.8302 0 72.0506 0Z" fill="#0089FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.0067 32.945H66.3293H44.5235H33.6205V23.999H68.0067V32.945ZM44.5233 53.9122V66.2129H68.2861V75.1589H33.6204V44.9662H44.5233H66.3291V53.9122H44.5233Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="147" height="24" fill="none">
<path
d="M30.827 24h5.224a2.24 2.24 0 0 0 2.241-2.238 2.24 2.24 0 0 0-2.24-2.239h-5.225a2.24 2.24 0 0 0-2.241 2.239A2.24 2.24 0 0 0 30.826 24zM7.465 24H2.241A2.24 2.24 0 0 1 0 21.762a2.24 2.24 0 0 1 2.24-2.239h5.225a2.24 2.24 0 1 1 0 4.477zm13.792-.014h-4.253a2.24 2.24 0 1 1 0-4.477h4.253a2.24 2.24 0 1 1 0 4.477z"
fill="#F23E94" />
<path
d="M28.796 17.558h5.03a2.24 2.24 0 1 0 0-4.477h-5.03a2.24 2.24 0 1 0 0 4.477zm-19.387 0H4.38a2.24 2.24 0 1 1 0-4.477h5.03a2.24 2.24 0 1 1 0 4.477zm13.466-.014h-7.486a2.24 2.24 0 0 1-2.24-2.239 2.24 2.24 0 0 1 2.24-2.239h7.486a2.24 2.24 0 0 1 0 4.477z"
fill="#6927DA" />
<path
d="M22.657 10.92h8.94a2.24 2.24 0 1 0 0-4.477h-8.94a2.24 2.24 0 1 0 0 4.477zm-6.497 0H6.608a2.24 2.24 0 1 1 0-4.477h9.552a2.24 2.24 0 1 1 0 4.477z"
fill="#1570EF" />
<path
d="M29.448 4.477h-5.041a2.24 2.24 0 0 1-2.241-2.238A2.24 2.24 0 0 1 24.407 0h5.041a2.24 2.24 0 1 1 0 4.477zm-15.656 0H8.751A2.24 2.24 0 1 1 8.75 0h5.041a2.24 2.24 0 1 1 0 4.477z"
fill="#2CE" />
<path
d="M73.648 10.81v6.455h-4.49v-5.753c0-1.97-.878-2.632-2.694-2.632-1.698 0-3.142.663-3.142 2.632v5.753h-4.49v-5.753c0-1.97-.878-2.632-2.694-2.632-1.698 0-3.142.663-3.142 2.632v5.753h-4.49v-11.7h4.49v2.067c1.249-1.521 2.889-2.457 5.153-2.457 2.069 0 3.943.858 4.743 3.003 1.308-1.833 3.045-3.003 5.583-3.003 2.772 0 5.173 1.54 5.173 5.635zm13.918 2.945v-.897c-1.503-.215-3.163-.39-4.666-.39-1.971 0-2.713.253-2.713.955 0 .663.586.917 2.264.917 1.406 0 3.826-.37 5.115-.585zm4.49-2.477v5.987h-4.49v-1.56c-2.089 1.287-4.666 1.755-6.657 1.755-3.064 0-5.407-.956-5.407-3.725 0-3.198 3.514-3.997 6.735-3.997 1.99 0 3.806.273 5.329.585-.117-1.619-1.582-2.028-3.631-2.028-2.089 0-4.08.292-6.852.936l-.624-3.062c2.537-.565 5.27-.994 8.315-.994 4.9 0 7.242 1.638 7.281 6.103zm13.976-5.713h4.548l-5.505 11.407c-1.639 3.393-3.416 5.363-7.027 5.363-1.288 0-2.928-.254-4.431-.683V18.24c1.737.39 2.967.585 3.767.585 1.503 0 2.147-.117 2.967-1.56h-2.108l-5.602-11.7h4.548l4.412 9.204 4.431-9.204zm17.744 5.85c0-1.716-1.191-2.535-3.572-2.535-2.382 0-3.573.819-3.573 2.535 0 1.716 1.191 2.535 3.573 2.535 2.381 0 3.572-.82 3.572-2.535zm4.685 0c0 4.719-3.377 6.24-6.403 6.24-2.128 0-4.236-.663-5.427-2.477v2.087h-4.489v-15.6h4.489v5.986c1.191-1.813 3.299-2.476 5.427-2.476 3.026 0 6.403 1.52 6.403 6.24zm6.461-1.482h7.32c-.508-1.346-1.757-1.833-3.69-1.833-1.893 0-3.142.487-3.63 1.833zm7.476 3.14h4.392c-.781 2.905-3.143 4.582-8.062 4.582-5.27 0-8.511-2.106-8.511-6.24s3.455-6.24 8.335-6.24c5.017 0 8.336 2.223 8.336 6.98h-12.123c.313 1.697 1.777 2.185 4.139 2.185 2.206 0 3.104-.507 3.494-1.268z"
fill="#141414" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,8 @@
<svg width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g fill="#4696e5">
<path d=" M 23.71 85.08 C 17.22 49.81 49.44 14.86 85.08 18.12 C 118.83 19.21 145.72 53.33 139.45 86.37 C 155.64 102.30 171.32 118.83 187.87 134.36 C 198.32 111.73 208.84 89.12 219.57 66.62 C 226.05 53.84 243.47 48.74 255.73 56.27 C 263.76 62.10 270.34 69.69 277.25 76.75 C 286.28 86.61 285.72 102.89 276.31 112.31 C 223.38 165.37 170.38 218.37 117.35 271.34 C 107.72 280.99 91.01 281.25 81.11 271.86 C 74.39 264.94 66.82 258.69 61.24 250.77 C 53.72 238.52 58.85 221.07 71.64 214.62 C 94.11 203.87 116.72 193.38 139.33 182.91 C 123.81 166.36 107.30 150.68 91.37 134.49 C 60.20 140.28 27.37 116.78 23.71 85.08 Z"/>
<path d=" M 205.21 201.23 C 225.32 181.36 260.88 181.11 281.14 200.86 C 299.25 218.75 317.37 236.65 335.10 254.93 C 356.73 278.01 352.01 318.70 326.03 336.56 C 320.07 330.47 313.73 324.65 308.12 318.28 C 323.86 309.39 328.76 286.18 316.63 272.39 C 301.73 256.95 286.30 242.03 271.24 226.75 C 264.49 219.65 256.80 212.00 246.37 211.52 C 224.65 208.64 205.52 233.36 214.49 253.58 C 221.09 266.81 234.22 275.12 243.62 286.24 C 240.43 295.96 238.09 306.13 238.29 316.46 C 225.55 304.29 213.16 291.73 200.89 279.09 C 180.97 257.57 183.10 220.45 205.21 201.23 Z"/>
<path d=" M 273.90 352.07 C 252.28 328.99 256.98 288.31 282.96 270.46 C 288.93 276.54 295.26 282.36 300.88 288.72 C 285.14 297.62 280.23 320.82 292.38 334.61 C 307.27 350.05 322.70 364.96 337.75 380.25 C 344.51 387.35 352.20 395.00 362.64 395.48 C 384.35 398.37 403.49 373.64 394.51 353.42 C 387.92 340.18 374.78 331.88 365.38 320.76 C 368.56 311.04 370.91 300.86 370.71 290.54 C 383.45 302.70 395.84 315.27 408.11 327.91 C 428.03 349.43 425.90 386.55 403.78 405.77 C 383.68 425.64 348.13 425.89 327.86 406.14 C 309.75 388.25 291.60 370.37 273.90 352.07 Z"/>
<path d=" M 422.11 403.83 C 431.96 394.07 441.60 384.06 451.66 374.51 C 460.90 383.74 471.89 392.70 474.89 406.11 C 480.16 429.97 484.08 454.13 488.76 478.12 C 490.00 483.41 484.47 488.29 479.35 486.63 C 454.66 481.52 429.55 478.12 405.14 471.84 C 393.17 467.97 385.20 457.75 376.55 449.27 C 386.39 439.49 396.13 429.60 406.06 419.91 C 416.37 433.45 435.74 414.00 422.11 403.83 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

View File

@@ -1,6 +1,12 @@
<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 width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2343_96406)">
<path d="M136 2.28882e-05H0L0.000144482 136H136V2.28882e-05ZM27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27ZM107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="black"/>
<path d="M27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27Z" fill="white"/>
<path d="M107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2343_96406">
<rect width="136" height="136" rx="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -9,6 +9,7 @@ import {
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -16,6 +17,7 @@ import {
createMariadb,
createMount,
deployMariadb,
findBackupsByDbId,
findMariadbById,
findProjectById,
findServerById,
@@ -211,8 +213,10 @@ export const mariadbRouter = createTRPCRouter({
});
}
const backups = await findBackupsByDbId(input.mariadbId, "mariadb");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await cancelJobs(backups),
async () => await removeMariadbById(input.mariadbId),
];

View File

@@ -9,6 +9,7 @@ import {
apiSaveExternalPortMongo,
apiUpdateMongo,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -16,6 +17,7 @@ import {
createMongo,
createMount,
deployMongo,
findBackupsByDbId,
findMongoById,
findProjectById,
removeMongoById,
@@ -252,9 +254,11 @@ export const mongoRouter = createTRPCRouter({
message: "You are not authorized to delete this mongo",
});
}
const backups = await findBackupsByDbId(input.mongoId, "mongo");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await cancelJobs(backups),
async () => await removeMongoById(input.mongoId),
];

View File

@@ -12,6 +12,7 @@ import {
import { TRPCError } from "@trpc/server";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -19,6 +20,7 @@ import {
createMount,
createMysql,
deployMySql,
findBackupsByDbId,
findMySqlById,
findProjectById,
removeMySqlById,
@@ -249,8 +251,10 @@ export const mysqlRouter = createTRPCRouter({
});
}
const backups = await findBackupsByDbId(input.mysqlId, "mysql");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await cancelJobs(backups),
async () => await removeMySqlById(input.mysqlId),
];

View File

@@ -14,6 +14,7 @@ import {
apiSaveExternalPortPostgres,
apiUpdatePostgres,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -21,6 +22,7 @@ import {
createMount,
createPostgres,
deployPostgres,
findBackupsByDbId,
findPostgresById,
findProjectById,
removePostgresById,
@@ -231,8 +233,11 @@ export const postgresRouter = createTRPCRouter({
});
}
const backups = await findBackupsByDbId(input.postgresId, "postgres");
const cleanupOperations = [
removeService(postgres.appName, postgres.serverId),
cancelJobs(backups),
removePostgresById(input.postgresId),
];

View File

@@ -244,7 +244,6 @@ export const redisRouter = createTRPCRouter({
message: "You are not authorized to delete this Redis",
});
}
const cleanupOperations = [
async () => await removeService(redis?.appName, redis.serverId),
async () => await removeRedisById(input.redisId),

View File

@@ -345,7 +345,7 @@ export const settingsRouter = createTRPCRouter({
writeConfig("middlewares", input.traefikConfig);
return true;
}),
getUpdateData: adminProcedure.mutation(async () => {
getUpdateData: protectedProcedure.mutation(async () => {
if (IS_CLOUD) {
return DEFAULT_UPDATE_DATA;
}
@@ -373,10 +373,10 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
getDokployVersion: adminProcedure.query(() => {
getDokployVersion: protectedProcedure.query(() => {
return packageInfo.version;
}),
getReleaseTag: adminProcedure.query(() => {
getReleaseTag: protectedProcedure.query(() => {
return getDokployImageTag();
}),
readDirectories: protectedProcedure

View File

@@ -1,3 +1,10 @@
import {
type BackupScheduleList,
IS_CLOUD,
removeScheduleBackup,
scheduleBackup,
} from "@dokploy/server/index";
type QueueJob =
| {
type: "backup";
@@ -59,3 +66,19 @@ export const updateJob = async (job: QueueJob) => {
throw error;
}
};
export const cancelJobs = async (backups: BackupScheduleList) => {
for (const backup of backups) {
if (backup.enabled) {
if (IS_CLOUD) {
await removeJob({
cronSchedule: backup.schedule,
backupId: backup.backupId,
type: "backup",
});
} else {
removeScheduleBackup(backup.backupId);
}
}
}
};

View File

@@ -4,6 +4,7 @@
@layer base {
:root {
--terminal-paste: rgba(0, 0, 0, 0.2);
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
@@ -51,6 +52,7 @@
}
.dark {
--terminal-paste: rgba(255, 255, 255, 0.2);
--background: 0 0% 0%;
--foreground: 0 0% 98%;
@@ -235,3 +237,8 @@
background-color: hsl(var(--muted-foreground) / 0.5);
}
}
.xterm-bg-257.xterm-fg-257 {
background-color: var(--terminal-paste) !important;
color: currentColor !important;
}

View File

@@ -0,0 +1,14 @@
version: '3.3'
services:
alist:
image: xhofe/alist:v3.41.0
volumes:
- alist-data:/opt/alist/data
environment:
- PUID=0
- PGID=0
- UMASK=022
restart: unless-stopped
volumes:
alist-data:

View File

@@ -0,0 +1,22 @@
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: 5244,
serviceName: "alist",
},
];
return {
domains,
};
}

View File

@@ -0,0 +1,31 @@
services:
answer:
image: apache/answer:1.4.1
ports:
- '80'
restart: on-failure
volumes:
- answer-data:/data
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- dokploy-network
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: answer
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
answer-data:
db-data:

View File

@@ -0,0 +1,33 @@
import {
type DomainSchema,
type Schema,
type Template,
generateHash,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const mainDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 9080,
serviceName: "answer",
},
];
const envs = [
`ANSWER_HOST=http://${mainDomain}`,
`SERVICE_HASH=${mainServiceHash}`,
];
const mounts: Template["mounts"] = [];
return {
envs,
mounts,
domains,
};
}

View File

@@ -0,0 +1,346 @@
x-custom-image: &custom_image
image: ${IMAGE_NAME:-docker.io/frappe/erpnext}:${VERSION:-version-15}
pull_policy: ${PULL_POLICY:-always}
deploy:
restart_policy:
condition: always
services:
backend:
<<: *custom_image
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
healthcheck:
test:
- CMD
- wait-for-it
- '0.0.0.0:8000'
interval: 2s
timeout: 10s
retries: 30
frontend:
<<: *custom_image
command:
- nginx-entrypoint.sh
depends_on:
backend:
condition: service_started
required: true
websocket:
condition: service_started
required: true
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host}
SOCKETIO: websocket:9000
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off"
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
healthcheck:
test:
- CMD
- wait-for-it
- '0.0.0.0:8080'
interval: 2s
timeout: 30s
retries: 30
queue-default:
<<: *custom_image
command:
- bench
- worker
- --queue
- default
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
healthcheck:
test:
- CMD
- wait-for-it
- 'redis-queue:6379'
interval: 2s
timeout: 10s
retries: 30
depends_on:
configurator:
condition: service_completed_successfully
required: true
queue-long:
<<: *custom_image
command:
- bench
- worker
- --queue
- long
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
healthcheck:
test:
- CMD
- wait-for-it
- 'redis-queue:6379'
interval: 2s
timeout: 10s
retries: 30
depends_on:
configurator:
condition: service_completed_successfully
required: true
queue-short:
<<: *custom_image
command:
- bench
- worker
- --queue
- short
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
healthcheck:
test:
- CMD
- wait-for-it
- 'redis-queue:6379'
interval: 2s
timeout: 10s
retries: 30
depends_on:
configurator:
condition: service_completed_successfully
required: true
scheduler:
<<: *custom_image
healthcheck:
test:
- CMD
- wait-for-it
- 'redis-queue:6379'
interval: 2s
timeout: 10s
retries: 30
command:
- bench
- schedule
depends_on:
configurator:
condition: service_completed_successfully
required: true
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
websocket:
<<: *custom_image
healthcheck:
test:
- CMD
- wait-for-it
- '0.0.0.0:9000'
interval: 2s
timeout: 10s
retries: 30
command:
- node
- /home/frappe/frappe-bench/apps/frappe/socketio.js
depends_on:
configurator:
condition: service_completed_successfully
required: true
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
configurator:
<<: *custom_image
deploy:
mode: replicated
replicas: ${CONFIGURE:-0}
restart_policy:
condition: none
entrypoint: ["bash", "-c"]
command:
- >
[[ $${REGENERATE_APPS_TXT} == "1" ]] && ls -1 apps > sites/apps.txt;
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && exit 0;
bench set-config -g db_host $$DB_HOST;
bench set-config -gp db_port $$DB_PORT;
bench set-config -g redis_cache "redis://$$REDIS_CACHE";
bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
bench set-config -g redis_socketio "redis://$$REDIS_QUEUE";
bench set-config -gp socketio_port $$SOCKETIO_PORT;
environment:
DB_HOST: db
DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000"
REGENERATE_APPS_TXT: "${REGENERATE_APPS_TXT:-0}"
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
create-site:
<<: *custom_image
deploy:
mode: replicated
replicas: ${CREATE_SITE:-0}
restart_policy:
condition: none
entrypoint: ["bash", "-c"]
command:
- >
wait-for-it -t 120 db:3306;
wait-for-it -t 120 redis-cache:6379;
wait-for-it -t 120 redis-queue:6379;
export start=`date +%s`;
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
do
echo "Waiting for sites/common_site_config.json to be created";
sleep 5;
if (( `date +%s`-start > 120 )); then
echo "could not find sites/common_site_config.json with required keys";
exit 1
fi
done;
echo "sites/common_site_config.json found";
[[ -d "sites/${SITE_NAME}" ]] && echo "${SITE_NAME} already exists" && exit 0;
bench new-site --mariadb-user-host-login-scope='%' --admin-password=$${ADMIN_PASSWORD} --db-root-username=root --db-root-password=$${DB_ROOT_PASSWORD} $${INSTALL_APP_ARGS} $${SITE_NAME};
volumes:
- sites:/home/frappe/frappe-bench/sites
environment:
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
INSTALL_APP_ARGS: ${INSTALL_APP_ARGS}
SITE_NAME: ${SITE_NAME}
networks:
- bench-network
migration:
<<: *custom_image
deploy:
mode: replicated
replicas: ${MIGRATE:-0}
restart_policy:
condition: none
entrypoint: ["bash", "-c"]
command:
- >
curl -f http://${SITE_NAME}:8080/api/method/ping || echo "Site busy" && exit 0;
bench --site all set-config -p maintenance_mode 1;
bench --site all set-config -p pause_scheduler 1;
bench --site all migrate;
bench --site all set-config -p maintenance_mode 0;
bench --site all set-config -p pause_scheduler 0;
volumes:
- sites:/home/frappe/frappe-bench/sites
networks:
- bench-network
db:
image: mariadb:10.6
deploy:
restart_policy:
condition: always
healthcheck:
test: mysqladmin ping -h localhost --password=${DB_ROOT_PASSWORD}
interval: 1s
retries: 20
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
volumes:
- db-data:/var/lib/mysql
networks:
- bench-network
redis-cache:
deploy:
restart_policy:
condition: always
image: redis:6.2-alpine
volumes:
- redis-cache-data:/data
networks:
- bench-network
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 5s
retries: 3
redis-queue:
deploy:
restart_policy:
condition: always
image: redis:6.2-alpine
volumes:
- redis-queue-data:/data
networks:
- bench-network
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 5s
retries: 3
redis-socketio:
deploy:
restart_policy:
condition: always
image: redis:6.2-alpine
volumes:
- redis-socketio-data:/data
networks:
- bench-network
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 5s
retries: 3
volumes:
db-data:
redis-cache-data:
redis-queue-data:
redis-socketio-data:
sites:
networks:
bench-network:

View File

@@ -0,0 +1,37 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const dbRootPassword = generatePassword(32);
const adminPassword = generatePassword(32);
const mainDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 8080,
serviceName: "frontend",
},
];
const envs = [
`SITE_NAME=${mainDomain}`,
`ADMIN_PASSWORD=${adminPassword}`,
`DB_ROOT_PASSWORD=${dbRootPassword}`,
"MIGRATE=1",
"CREATE_SITE=1",
"CONFIGURE=1",
"REGENERATE_APPS_TXT=1",
"INSTALL_APP_ARGS=--install-app erpnext",
"IMAGE_NAME=docker.io/frappe/erpnext",
"VERSION=version-15",
"FRAPPE_SITE_NAME_HEADER=",
];
return { envs, domains };
}

View File

@@ -0,0 +1,8 @@
services:
glance:
image: glanceapp/glance
volumes:
- ../files/app/glance.yml:/app/glance.yml
ports:
- 8080
restart: unless-stopped

View File

@@ -0,0 +1,108 @@
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: 8080,
serviceName: "glance",
},
];
const mounts: Template["mounts"] = [
{
filePath: "/app/glance.yml",
content: `
branding:
hide-footer: true
logo-text: P
pages:
- name: Home
columns:
- size: small
widgets:
- type: calendar
- type: releases
show-source-icon: true
repositories:
- Dokploy/dokploy
- n8n-io/n8n
- Budibase/budibase
- home-assistant/core
- tidbyt/pixlet
- type: twitch-channels
channels:
- nmplol
- extraemily
- qtcinderella
- ludwig
- timthetatman
- mizkif
- size: full
widgets:
- type: hacker-news
- type: videos
style: grid-cards
channels:
- UC3GzdWYwUYI1ACxuP9Nm-eg
- UCGbg3DjQdcqWwqOLHpYHXIg
- UC24RSoLcjiNZbQcT54j5l7Q
limit: 3
- type: rss
limit: 10
collapse-after: 3
cache: 3h
feeds:
- url: https://daringfireball.net/feeds/main
title: Daring Fireball
- size: small
widgets:
- type: weather
location: Gansevoort, New York, United States
show-area-name: false
units: imperial
hour-format: 12h
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: VOO
name: Vanguard
- symbol: BTC-USD
name: Bitcoin
- symbol: ETH-USD
name: Etherium
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- symbol: GOOGL
name: Google
- symbol: AMD
name: AMD
- symbol: TSLA
name: Tesla`,
},
];
return {
domains,
mounts,
};
}

View File

@@ -0,0 +1,11 @@
services:
homarr:
image: ghcr.io/homarr-labs/homarr:latest
restart: unless-stopped
volumes:
# - /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration
- ../homarr/appdata:/appdata
environment:
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
ports:
- 7575

View File

@@ -0,0 +1,27 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const secretKey = generatePassword(64);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 7575,
serviceName: "homarr",
},
];
const envs = [`SECRET_ENCRYPTION_KEY=${secretKey}`];
return {
domains,
envs,
};
}

View File

@@ -0,0 +1,37 @@
services:
app:
image: ghcr.io/maybe-finance/maybe:sha-68c570eed8810fd59b5b33cca51bbad5eabb4cb4
restart: unless-stopped
volumes:
- ../files/uploads:/app/uploads
environment:
DATABASE_URL: postgresql://maybe:maybe@db:5432/maybe
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
SELF_HOSTED: true
SYNTH_API_KEY: ${SYNTH_API_KEY}
RAILS_FORCE_SSL: "false"
RAILS_ASSUME_SSL: "false"
GOOD_JOB_EXECUTION_MODE: async
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- dokploy-network
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: maybe
POSTGRES_USER: maybe
POSTGRES_PASSWORD: maybe
volumes:
db-data:

View File

@@ -0,0 +1,43 @@
import {
type DomainSchema,
type Schema,
type Template,
generateBase64,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const secretKeyBase = generateBase64(64);
const synthApiKey = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 3000,
serviceName: "app",
},
];
const envs = [
`SECRET_KEY_BASE=${secretKeyBase}`,
"SELF_HOSTED=true",
`SYNTH_API_KEY=${synthApiKey}`,
"RAILS_FORCE_SSL=false",
"RAILS_ASSUME_SSL=false",
"GOOD_JOB_EXECUTION_MODE=async",
];
const mounts: Template["mounts"] = [
{
filePath: "./uploads",
content: "This is where user uploads will be stored",
},
];
return {
envs,
mounts,
domains,
};
}

View File

@@ -0,0 +1,29 @@
services:
shlink:
image: shlinkio/shlink:stable
environment:
- INITIAL_API_KEY=${INITIAL_API_KEY}
- DEFAULT_DOMAIN=${DEFAULT_DOMAIN}
# Note: you should also update SHLINK_SERVER_URL in the shlink-web service.
- IS_HTTPS_ENABLED=false
volumes:
- shlink-data:/etc/shlink/data
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/rest/v3/health"]
interval: 30s
timeout: 10s
retries: 3
shlink-web:
image: shlinkio/shlink-web-client
environment:
- SHLINK_SERVER_API_KEY=${INITIAL_API_KEY}
# Note: if you've set IS_HTTPS_ENABLED=true, change http to https.
- SHLINK_SERVER_URL=http://${DEFAULT_DOMAIN}
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080"]
interval: 30s
timeout: 10s
retries: 3
volumes:
shlink-data:

View File

@@ -0,0 +1,35 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const defaultDomain = generateRandomDomain(schema);
const initialApiKey = generatePassword(30);
const domains: DomainSchema[] = [
{
host: `web-${defaultDomain}`,
port: 8080,
serviceName: "shlink-web",
},
{
host: defaultDomain,
port: 8080,
serviceName: "shlink",
},
];
const envs = [
`INITIAL_API_KEY=${initialApiKey}`,
`DEFAULT_DOMAIN=${defaultDomain}`,
];
return {
envs,
domains,
};
}

View File

@@ -0,0 +1,9 @@
services:
server:
image: ghcr.io/spacedriveapp/spacedrive/server:latest
ports:
- 8080
environment:
- SD_AUTH=${SD_USERNAME}:${SD_PASSWORD}
volumes:
- /var/spacedrive:/var/spacedrive

View File

@@ -0,0 +1,28 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const randomDomain = generateRandomDomain(schema);
const secretKey = generatePassword();
const randomUsername = "admin"; // Default username
const domains: DomainSchema[] = [
{
host: randomDomain,
port: 8080,
serviceName: "server",
},
];
const envs = [`SD_USERNAME=${randomUsername}`, `SD_PASSWORD=${secretKey}`];
return {
envs,
domains,
};
}

View File

@@ -13,8 +13,11 @@ services:
image: amancevice/superset
restart: always
depends_on:
- db
- redis
- superset_postgres
- superset_redis
volumes:
# This superset_config.py can be edited in Dokploy's UI Advanced -> Volume Mount
- ../files/superset/superset_config.py:/etc/superset/superset_config.py
environment:
SECRET_KEY: ${SECRET_KEY}
MAPBOX_API_KEY: ${MAPBOX_API_KEY}
@@ -22,11 +25,11 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
REDIS_PASSWORD: ${REDIS_PASSWORD}
volumes:
# Note: superset_config.py can be edited in Dokploy's UI Volume Mount
- ../files/superset/superset_config.py:/etc/superset/superset_config.py
# Ensure the hosts matches your service names below.
POSTGRES_HOST: superset_postgres
REDIS_HOST: superset_redis
db:
superset_postgres:
image: postgres
restart: always
environment:
@@ -34,7 +37,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres:/var/lib/postgresql/data
- superset_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 30s
@@ -43,11 +46,11 @@ services:
networks:
- dokploy-network
redis:
superset_redis:
image: redis
restart: always
volumes:
- redis:/data
- superset_redis_data:/data
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
@@ -58,5 +61,5 @@ services:
- dokploy-network
volumes:
postgres:
redis:
superset_postgres_data:
superset_redis_data:

View File

@@ -52,14 +52,15 @@ CACHE_CONFIG = {
"CACHE_REDIS_HOST": "redis",
"CACHE_REDIS_PORT": 6379,
"CACHE_REDIS_DB": 1,
"CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@redis:6379/1",
"CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@{os.getenv('REDIS_HOST')}:6379/1",
}
FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"}
EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"}
SQLALCHEMY_TRACK_MODIFICATIONS = True
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}"
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@{os.getenv('POSTGRES_HOST')}:5432/{os.getenv('POSTGRES_DB')}"
# Uncomment if you want to load example data (using "superset load_examples") at the
# same location as your metadata postgresql instance. Otherwise, the default sqlite
# will be used, which will not persist in volume when restarting superset by default.

View File

@@ -538,7 +538,7 @@ export const templates: TemplateData[] = [
website: "https://filebrowser.org/",
docs: "https://filebrowser.org/",
},
tags: ["file", "manager"],
tags: ["file-manager", "storage"],
load: () => import("./filebrowser/index").then((m) => m.generate),
},
{
@@ -834,7 +834,7 @@ export const templates: TemplateData[] = [
website: "https://nextcloud.com/",
docs: "https://docs.nextcloud.com/",
},
tags: ["file", "sync"],
tags: ["file-manager", "sync"],
load: () => import("./nextcloud-aio/index").then((m) => m.generate),
},
{
@@ -1074,7 +1074,7 @@ export const templates: TemplateData[] = [
website: "https://penpot.app/",
docs: "https://docs.penpot.app/",
},
tags: ["desing", "collaboration"],
tags: ["design", "collaboration"],
load: () => import("./penpot/index").then((m) => m.generate),
},
{
@@ -1097,7 +1097,7 @@ export const templates: TemplateData[] = [
name: "Unsend",
version: "v1.2.4",
description: "Open source alternative to Resend,Sendgrid, Postmark etc. ",
logo: "unsend.png", // we defined the name and the extension of the logo
logo: "unsend.png",
links: {
github: "https://github.com/unsend-dev/unsend",
website: "https://unsend.dev/",
@@ -1276,11 +1276,11 @@ export const templates: TemplateData[] = [
version: "latest",
description:
"CouchDB is a document-oriented NoSQL database that excels at replication and horizontal scaling.",
logo: "couchdb.png", // we defined the name and the extension of the logo
logo: "couchdb.png",
links: {
github: "lorem",
website: "lorem",
docs: "lorem",
github: "https://github.com/apache/couchdb",
website: "https://couchdb.apache.org/",
docs: "https://docs.couchdb.org/en/stable/",
},
tags: ["database", "storage"],
load: () => import("./couchdb/index").then((m) => m.generate),
@@ -1312,4 +1312,130 @@ export const templates: TemplateData[] = [
tags: ["analytics", "bi", "dashboard", "database", "sql"],
load: () => import("./superset/index").then((m) => m.generate),
},
{
id: "glance",
name: "Glance",
version: "latest",
description:
"A self-hosted dashboard that puts all your feeds in one place. Features RSS feeds, weather, bookmarks, site monitoring, and more in a minimal, fast interface.",
logo: "glance.png",
links: {
github: "https://github.com/glanceapp/glance",
docs: "https://github.com/glanceapp/glance/blob/main/docs/configuration.md",
},
tags: ["dashboard", "monitoring", "widgets", "rss"],
load: () => import("./glance/index").then((m) => m.generate),
},
{
id: "homarr",
name: "Homarr",
version: "latest",
description:
"A sleek, modern dashboard that puts all your apps and services in one place with Docker integration.",
logo: "homarr.png",
links: {
github: "https://github.com/homarr-labs/homarr",
docs: "https://homarr.dev/docs/getting-started/installation/docker",
website: "https://homarr.dev/",
},
tags: ["dashboard", "monitoring"],
load: () => import("./homarr/index").then((m) => m.generate),
},
{
id: "erpnext",
name: "ERPNext",
version: "version-15",
description: "100% Open Source and highly customizable ERP software.",
logo: "erpnext.svg",
links: {
github: "https://github.com/frappe/erpnext",
docs: "https://docs.frappe.io/erpnext",
website: "https://erpnext.com",
},
tags: [
"erp",
"accounts",
"manufacturing",
"retail",
"sales",
"pos",
"hrms",
],
load: () => import("./erpnext/index").then((m) => m.generate),
},
{
id: "maybe",
name: "Maybe",
version: "latest",
description:
"Maybe is a self-hosted finance tracking application designed to simplify budgeting and expenses.",
logo: "maybe.svg",
links: {
github: "https://github.com/maybe-finance/maybe",
website: "https://maybe.finance/",
docs: "https://docs.maybe.finance/",
},
tags: ["finance", "self-hosted"],
load: () => import("./maybe/index").then((m) => m.generate),
},
{
id: "spacedrive",
name: "Spacedrive",
version: "latest",
description:
"Spacedrive is a cross-platform file manager. It connects your devices together to help you organize files from anywhere. powered by a virtual distributed filesystem (VDFS) written in Rust. Organize files across many devices in one place.",
links: {
github: "https://github.com/spacedriveapp/spacedrive",
website: "https://spacedrive.com/",
docs: "https://www.spacedrive.com/docs/product/getting-started/introduction",
},
logo: "spacedrive.png",
tags: ["file-manager", "vdfs", "storage"],
load: () => import("./spacedrive/index").then((m) => m.generate),
},
{
id: "alist",
name: "AList",
version: "v3.41.0",
description:
"🗂A file list/WebDAV program that supports multiple storages, powered by Gin and Solidjs.",
logo: "alist.svg",
links: {
github: "https://github.com/AlistGo/alist",
website: "https://alist.nn.ci",
docs: "https://alist.nn.ci/guide/install/docker.html",
},
tags: ["file", "webdav", "storage"],
load: () => import("./alist/index").then((m) => m.generate),
},
{
id: "answer",
name: "Answer",
version: "v1.4.1",
description:
"Answer is an open-source Q&A platform for building a self-hosted question-and-answer service.",
logo: "answer.png",
links: {
github: "https://github.com/apache/answer",
website: "https://answer.apache.org/",
docs: "https://answer.apache.org/docs",
},
tags: ["q&a", "self-hosted"],
load: () => import("./answer/index").then((m) => m.generate),
},
{
id: "shlink",
name: "Shlink",
version: "stable",
description:
"URL shortener that can be used to serve shortened URLs under your own domain.",
logo: "shlink.svg",
links: {
github: "https://github.com/shlinkio/shlink",
website: "https://shlink.io",
docs: "https://shlink.io/documentation",
},
tags: ["sharing", "shortener", "url"],
load: () => import("./shlink/index").then((m) => m.generate),
},
];

View File

@@ -33,7 +33,7 @@ export const admins = pgTable("admin", {
serversQuantity: integer("serversQuantity").notNull().default(0),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(true),
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),

View File

@@ -47,6 +47,7 @@ export const deployments = pgTable("deployment", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
errorMessage: text("errorMessage"),
});
export const deploymentsRelations = relations(deployments, ({ one }) => ({

View File

@@ -451,7 +451,7 @@ export const deployPreviewApplication = async ({
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = application.previewEnv;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs;
const admin = await findAdminById(application.project.adminId);
@@ -564,7 +564,7 @@ export const deployRemotePreviewApplication = async ({
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = application.previewEnv;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs;
if (application.serverId) {
@@ -577,6 +577,8 @@ export const deployRemotePreviewApplication = async ({
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
serverId: application.serverId,
logPath: deployment.logPath,
});

View File

@@ -2,11 +2,13 @@ import { db } from "@dokploy/server/db";
import { type apiCreateBackup, backups } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { removeScheduleBackup, scheduleBackup } from "../utils/backups/utils";
export type Backup = typeof backups.$inferSelect;
export type BackupSchedule = Awaited<ReturnType<typeof findBackupById>>;
export type BackupScheduleList = Awaited<ReturnType<typeof findBackupsByDbId>>;
export const createBackup = async (input: typeof apiCreateBackup._type) => {
const newBackup = await db
.insert(backups)
@@ -69,3 +71,20 @@ export const removeBackupById = async (backupId: string) => {
return result[0];
};
export const findBackupsByDbId = async (
id: string,
type: "postgres" | "mysql" | "mariadb" | "mongo",
) => {
const result = await db.query.backups.findMany({
where: eq(backups[`${type}Id`], id),
with: {
postgres: true,
mysql: true,
mariadb: true,
mongo: true,
destination: true,
},
});
return result || [];
};

View File

@@ -98,6 +98,17 @@ export const createDeployment = async (
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
applicationId: deployment.applicationId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
})
.returning();
await updateApplicationStatus(application.applicationId, "error");
console.log(error);
throw new TRPCError({
@@ -164,6 +175,17 @@ export const createDeploymentPreview = async (
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
previewDeploymentId: deployment.previewDeploymentId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
})
.returning();
await updatePreviewDeployment(deployment.previewDeploymentId, {
previewStatus: "error",
});
@@ -226,6 +248,17 @@ echo "Initializing deployment" >> ${logFilePath};
}
return deploymentCreate[0];
} catch (error) {
await db
.insert(deployments)
.values({
composeId: deployment.composeId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
})
.returning();
await updateCompose(compose.composeId, {
composeStatus: "error",
});

View File

@@ -64,7 +64,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the mount",
message: `Error ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
@@ -91,7 +91,7 @@ export const createFileMount = async (mountId: string) => {
console.log(`Error creating the file mount: ${error}`);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the mount",
message: `Error creating the mount ${error instanceof Error ? error.message : error}`,
cause: error,
});
}

View File

@@ -216,8 +216,8 @@ echo "$json_output"
};
export const cleanupFullDocker = async (serverId?: string | null) => {
const cleanupImages = "docker image prune --all --force";
const cleanupVolumes = "docker volume prune --all --force";
const cleanupImages = "docker image prune --force";
const cleanupVolumes = "docker volume prune --force";
const cleanupContainers = "docker container prune --force";
const cleanupSystem = "docker system prune --all --force --volumes";
const cleanupBuilder = "docker builder prune --all --force";

View File

@@ -189,10 +189,12 @@ export const getDefaultTraefikConfig = () => {
: {
swarm: {
exposedByDefault: false,
watch: false,
watch: true,
},
docker: {
exposedByDefault: false,
watch: true,
network: "dokploy-network",
},
}),
file: {
@@ -243,10 +245,12 @@ export const getDefaultServerTraefikConfig = () => {
providers: {
swarm: {
exposedByDefault: false,
watch: false,
watch: true,
},
docker: {
exposedByDefault: false,
watch: true,
network: "dokploy-network",
},
file: {
directory: "/etc/dokploy/traefik/dynamic",

View File

@@ -144,10 +144,11 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
};
export const cleanUpUnusedImages = async (serverId?: string) => {
try {
const command = "docker image prune --force";
if (serverId) {
await execAsyncRemote(serverId, "docker image prune --all --force");
await execAsyncRemote(serverId, command);
} else {
await execAsync("docker image prune --all --force");
await execAsync(command);
}
} catch (error) {
console.error(error);
@@ -157,10 +158,11 @@ export const cleanUpUnusedImages = async (serverId?: string) => {
export const cleanStoppedContainers = async (serverId?: string) => {
try {
const command = "docker container prune --force";
if (serverId) {
await execAsyncRemote(serverId, "docker container prune --force");
await execAsyncRemote(serverId, command);
} else {
await execAsync("docker container prune --force");
await execAsync(command);
}
} catch (error) {
console.error(error);
@@ -170,10 +172,11 @@ export const cleanStoppedContainers = async (serverId?: string) => {
export const cleanUpUnusedVolumes = async (serverId?: string) => {
try {
const command = "docker volume prune --force";
if (serverId) {
await execAsyncRemote(serverId, "docker volume prune --all --force");
await execAsyncRemote(serverId, command);
} else {
await execAsync("docker volume prune --all --force");
await execAsync(command);
}
} catch (error) {
console.error(error);
@@ -199,21 +202,20 @@ export const cleanUpInactiveContainers = async () => {
};
export const cleanUpDockerBuilder = async (serverId?: string) => {
const command = "docker builder prune --all --force";
if (serverId) {
await execAsyncRemote(serverId, "docker builder prune --all --force");
await execAsyncRemote(serverId, command);
} else {
await execAsync("docker builder prune --all --force");
await execAsync(command);
}
};
export const cleanUpSystemPrune = async (serverId?: string) => {
const command = "docker system prune --all --force --volumes";
if (serverId) {
await execAsyncRemote(
serverId,
"docker system prune --all --force --volumes",
);
await execAsyncRemote(serverId, command);
} else {
await execAsync("docker system prune --all --force --volumes");
await execAsync(command);
}
};

View File

@@ -69,6 +69,7 @@ export const cloneGitRepository = async (
});
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
await spawnAsync(
"git",
[
@@ -91,7 +92,7 @@ export const cloneGitRepository = async (
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
},
@@ -168,7 +169,8 @@ export const getCustomGitCloneCommand = async (
);
if (customGitSSHKeyId) {
const sshKey = await findSSHKeyById(customGitSSHKeyId);
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
const { port } = sanitizeRepoPathSSH(customGitUrl);
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
command.push(
`
echo "${sshKey.privateKey}" > /tmp/id_rsa
@@ -304,6 +306,7 @@ export const cloneGitRawRepository = async (entity: {
});
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
await spawnAsync(
"git",
[
@@ -322,7 +325,7 @@ export const cloneGitRawRepository = async (entity: {
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
},
@@ -381,7 +384,8 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
command.push(`mkdir -p ${outputPath};`);
if (customGitSSHKeyId) {
const sshKey = await findSSHKeyById(customGitSSHKeyId);
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
const { port } = sanitizeRepoPathSSH(customGitUrl);
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
command.push(
`
echo "${sshKey.privateKey}" > /tmp/id_rsa

63
pnpm-lock.yaml generated
View File

@@ -124,6 +124,12 @@ importers:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.9.0(react-hook-form@7.52.1(react@18.2.0))
'@lucia-auth/adapter-drizzle':
specifier: 1.0.7
version: 1.0.7(lucia@3.2.0)
'@octokit/auth-app':
specifier: ^6.0.4
version: 6.1.1
'@octokit/webhooks':
specifier: ^13.2.7
version: 13.3.0
@@ -184,6 +190,9 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@react-email/components':
specifier: ^0.0.21
version: 0.0.21(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@stepperize/react':
specifier: 4.0.1
version: 4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -217,6 +226,9 @@ importers:
'@xterm/addon-attach':
specifier: 0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
'@xterm/addon-clipboard':
specifier: 0.1.0
version: 0.1.0(@xterm/xterm@5.5.0)
'@xterm/xterm':
specifier: ^5.4.0
version: 5.5.0
@@ -226,6 +238,12 @@ importers:
bcrypt:
specifier: 5.1.1
version: 5.1.1
bl:
specifier: 6.0.11
version: 6.0.11
boxen:
specifier: ^7.1.1
version: 7.1.1
bullmq:
specifier: 5.4.2
version: 5.4.2
@@ -247,6 +265,9 @@ importers:
date-fns:
specifier: 3.6.0
version: 3.6.0
dockerode:
specifier: 4.0.2
version: 4.0.2
dotenv:
specifier: 16.4.5
version: 16.4.5
@@ -259,6 +280,9 @@ importers:
fancy-ansi:
specifier: ^0.1.3
version: 0.1.3
hi-base32:
specifier: ^0.5.1
version: 0.5.1
i18next:
specifier: ^23.16.4
version: 23.16.5
@@ -292,21 +316,33 @@ importers:
next-themes:
specifier: ^0.2.1
version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
node-os-utils:
specifier: 1.3.7
version: 1.3.7
node-pty:
specifier: 1.0.0
version: 1.0.0
node-schedule:
specifier: 2.1.1
version: 2.1.1
nodemailer:
specifier: 6.9.14
version: 6.9.14
octokit:
specifier: 3.1.2
version: 3.1.2
otpauth:
specifier: ^9.2.3
version: 9.3.4
postgres:
specifier: 3.4.4
version: 3.4.4
public-ip:
specifier: 6.0.2
version: 6.0.2
qrcode:
specifier: ^1.5.3
version: 1.5.4
react:
specifier: 18.2.0
version: 18.2.0
@@ -325,6 +361,9 @@ importers:
recharts:
specifier: ^2.12.7
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
slugify:
specifier: ^1.6.6
version: 1.6.6
@@ -386,9 +425,18 @@ importers:
'@types/node':
specifier: ^18.17.0
version: 18.19.42
'@types/node-os-utils':
specifier: 1.3.4
version: 1.3.4
'@types/node-schedule':
specifier: 2.1.6
version: 2.1.6
'@types/nodemailer':
specifier: ^6.4.15
version: 6.4.16
'@types/qrcode':
specifier: ^1.5.5
version: 1.5.5
'@types/react':
specifier: 18.3.5
version: 18.3.5
@@ -3503,6 +3551,11 @@ packages:
peerDependencies:
'@xterm/xterm': ^5.0.0
'@xterm/addon-clipboard@0.1.0':
resolution: {integrity: sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==}
peerDependencies:
'@xterm/xterm': ^5.4.0
'@xterm/xterm@5.5.0':
resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==}
@@ -5011,6 +5064,9 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
js-beautify@1.15.1:
resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==}
engines: {node: '>=14'}
@@ -9894,6 +9950,11 @@ snapshots:
dependencies:
'@xterm/xterm': 5.5.0
'@xterm/addon-clipboard@0.1.0(@xterm/xterm@5.5.0)':
dependencies:
'@xterm/xterm': 5.5.0
js-base64: 3.7.7
'@xterm/xterm@5.5.0': {}
'@xtuc/ieee754@1.2.0': {}
@@ -11410,6 +11471,8 @@ snapshots:
joycon@3.1.1: {}
js-base64@3.7.7: {}
js-beautify@1.15.1:
dependencies:
config-chain: 1.1.13