refactor: lint

This commit is contained in:
Mauricio Siu
2024-12-23 02:28:40 -06:00
parent c6892ba188
commit ed543e5397
37 changed files with 1743 additions and 1745 deletions

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -5,11 +6,10 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../../docker/logs/terminal-line"; import { TerminalLine } from "../../docker/logs/terminal-line";
import { LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { Badge } from "@/components/ui/badge";
import { Loader2 } from "lucide-react";
interface Props { interface Props {
logPath: string | null; logPath: string | null;
@@ -24,21 +24,20 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const [autoScroll, setAutoScroll] = useState(true); const [autoScroll, setAutoScroll] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => { const scrollToBottom = () => {
if (autoScroll && scrollRef.current) { if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
} }
}; };
const handleScroll = () => { const handleScroll = () => {
if (!scrollRef.current) return; if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom); setAutoScroll(isAtBottom);
}; };
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
@@ -69,7 +68,6 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
}; };
}, [logPath, open]); }, [logPath, open]);
useEffect(() => { useEffect(() => {
const logs = parseLogs(data); const logs = parseLogs(data);
setFilteredLogs(logs); setFilteredLogs(logs);
@@ -77,12 +75,11 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
return ( return (
<Dialog <Dialog
@@ -104,27 +101,28 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
<DialogHeader> <DialogHeader>
<DialogTitle>Deployment</DialogTitle> <DialogTitle>Deployment</DialogTitle>
<DialogDescription> <DialogDescription>
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge> See all the details of this deployment |{" "}
<Badge variant="blank" className="text-xs">
{filteredLogs.length} lines
</Badge>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div <div
ref={scrollRef} ref={scrollRef}
onScroll={handleScroll} onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar" className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
> { >
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( {" "}
<TerminalLine {filteredLogs.length > 0 ? (
key={index} filteredLogs.map((log: LogLine, index: number) => (
log={log} <TerminalLine key={index} log={log} noTimestamp />
noTimestamp ))
/> ) : (
)) : <div className="flex justify-center items-center h-full text-muted-foreground">
( <Loader2 className="h-6 w-6 animate-spin" />
<div className="flex justify-center items-center h-full text-muted-foreground"> </div>
<Loader2 className="h-6 w-6 animate-spin" /> )}
</div>
)}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -26,7 +26,9 @@ export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="sm:w-auto w-full" size="sm" variant="outline">View Builds</Button> <Button className="sm:w-auto w-full" size="sm" variant="outline">
View Builds
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl"> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogHeader> <DialogHeader>

View File

@@ -1,237 +1,237 @@
import React from "react"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { import {
Clock, Card,
GitBranch, CardContent,
GitPullRequest, CardDescription,
Pencil, CardHeader,
RocketIcon, CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { api } from "@/utils/api";
import {
Clock,
GitBranch,
GitPullRequest,
Pencil,
RocketIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import React from "react";
import { DialogAction } from "@/components/shared/dialog-action";
import { api } from "@/utils/api";
import { ShowPreviewBuilds } from "./show-preview-builds";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { toast } from "sonner"; import { toast } from "sonner";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { AddPreviewDomain } from "./add-preview-domain"; import { AddPreviewDomain } from "./add-preview-domain";
import { import { ShowPreviewBuilds } from "./show-preview-builds";
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ShowPreviewSettings } from "./show-preview-settings"; import { ShowPreviewSettings } from "./show-preview-settings";
interface Props { interface Props {
applicationId: string; applicationId: string;
} }
export const ShowPreviewDeployments = ({ applicationId }: Props) => { export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery({ applicationId }); const { data } = api.application.one.useQuery({ applicationId });
const { mutateAsync: deletePreviewDeployment, isLoading } = const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation(); api.previewDeployment.delete.useMutation();
const { data: previewDeployments, refetch: refetchPreviewDeployments } = const { data: previewDeployments, refetch: refetchPreviewDeployments } =
api.previewDeployment.all.useQuery( api.previewDeployment.all.useQuery(
{ applicationId }, { applicationId },
{ {
enabled: !!applicationId, enabled: !!applicationId,
} },
); );
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
deletePreviewDeployment({ deletePreviewDeployment({
previewDeploymentId: previewDeploymentId, previewDeploymentId: previewDeploymentId,
}) })
.then(() => { .then(() => {
refetchPreviewDeployments(); refetchPreviewDeployments();
toast.success("Preview deployment deleted"); toast.success("Preview deployment deleted");
}) })
.catch((error) => { .catch((error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
return ( return (
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2"> <CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<CardTitle className="text-xl">Preview Deployments</CardTitle> <CardTitle className="text-xl">Preview Deployments</CardTitle>
<CardDescription>See all the preview deployments</CardDescription> <CardDescription>See all the preview deployments</CardDescription>
</div> </div>
{data?.isPreviewDeploymentsActive && ( {data?.isPreviewDeploymentsActive && (
<ShowPreviewSettings applicationId={applicationId} /> <ShowPreviewSettings applicationId={applicationId} />
)} )}
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{data?.isPreviewDeploymentsActive ? ( {data?.isPreviewDeploymentsActive ? (
<> <>
<div className="flex flex-col gap-2 text-sm"> <div className="flex flex-col gap-2 text-sm">
<span> <span>
Preview deployments are a way to test your application before it Preview deployments are a way to test your application before it
is deployed to production. It will create a new deployment for is deployed to production. It will create a new deployment for
each pull request you create. each pull request you create.
</span> </span>
</div> </div>
{!previewDeployments?.length ? ( {!previewDeployments?.length ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10"> <div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<RocketIcon className="size-8 text-muted-foreground" /> <RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
No preview deployments found No preview deployments found
</span> </span>
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{previewDeployments.map((previewDeployment) => ( {previewDeployments.map((previewDeployment) => (
<div <div
key={previewDeployment.previewDeploymentId} key={previewDeployment.previewDeploymentId}
className="w-full border rounded-xl" className="w-full border rounded-xl"
> >
<div className="md:p-6 p-2 md:pb-3 flex flex-row items-center justify-between"> <div className="md:p-6 p-2 md:pb-3 flex flex-row items-center justify-between">
<span className="text-lg font-bold"> <span className="text-lg font-bold">
{previewDeployment.pullRequestTitle} {previewDeployment.pullRequestTitle}
</span> </span>
<Badge <Badge
variant="outline" variant="outline"
className="text-sm font-medium gap-x-2" className="text-sm font-medium gap-x-2"
> >
<StatusTooltip <StatusTooltip
status={previewDeployment.previewStatus} status={previewDeployment.previewStatus}
className="size-2.5" className="size-2.5"
/> />
{previewDeployment.previewStatus {previewDeployment.previewStatus
?.replace("running", "Running") ?.replace("running", "Running")
.replace("done", "Done") .replace("done", "Done")
.replace("error", "Error") .replace("error", "Error")
.replace("idle", "Idle") || "Idle"} .replace("idle", "Idle") || "Idle"}
</Badge> </Badge>
</div> </div>
<div className="md:p-6 p-2 md:pt-0 space-y-4"> <div className="md:p-6 p-2 md:pt-0 space-y-4">
<div className="flex sm:flex-row flex-col items-center gap-2"> <div className="flex sm:flex-row flex-col items-center gap-2">
<Link <Link
href={`http://${previewDeployment.domain?.host}`} href={`http://${previewDeployment.domain?.host}`}
target="_blank" target="_blank"
className="text-sm text-blue-500/95 hover:underline gap-2 flex w-full sm:flex-row flex-col items-center justify-between rounded-lg border p-2" className="text-sm text-blue-500/95 hover:underline gap-2 flex w-full sm:flex-row flex-col items-center justify-between rounded-lg border p-2"
> >
{previewDeployment.domain?.host} {previewDeployment.domain?.host}
</Link> </Link>
<AddPreviewDomain <AddPreviewDomain
previewDeploymentId={ previewDeploymentId={
previewDeployment.previewDeploymentId previewDeployment.previewDeploymentId
} }
domainId={previewDeployment.domain?.domainId} domainId={previewDeployment.domain?.domainId}
> >
<Button <Button
className="sm:w-auto w-full" className="sm:w-auto w-full"
size="sm" size="sm"
variant="outline" variant="outline"
> >
<Pencil className="size-4" /> <Pencil className="size-4" />
Edit Edit
</Button> </Button>
</AddPreviewDomain> </AddPreviewDomain>
</div> </div>
<div className="flex sm:flex-row text-sm flex-col items-center justify-between"> <div className="flex sm:flex-row text-sm flex-col items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<GitBranch className="size-5 text-gray-400" /> <GitBranch className="size-5 text-gray-400" />
<span>Branch:</span> <span>Branch:</span>
<Badge className="p-2" variant="blank"> <Badge className="p-2" variant="blank">
{previewDeployment.branch} {previewDeployment.branch}
</Badge> </Badge>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Clock className="size-5 text-gray-400" /> <Clock className="size-5 text-gray-400" />
<span>Deployed:</span> <span>Deployed:</span>
<Badge className="p-2" variant="blank"> <Badge className="p-2" variant="blank">
<DateTooltip date={previewDeployment.createdAt} /> <DateTooltip date={previewDeployment.createdAt} />
</Badge> </Badge>
</div> </div>
</div> </div>
<Separator /> <Separator />
<div className="rounded-lg bg-muted p-4"> <div className="rounded-lg bg-muted p-4">
<h3 className="mb-2 text-sm font-medium"> <h3 className="mb-2 text-sm font-medium">
Pull Request Pull Request
</h3> </h3>
<div className="flex items-center space-x-2 text-sm text-muted-foreground"> <div className="flex items-center space-x-2 text-sm text-muted-foreground">
<GitPullRequest className="size-5 text-gray-400" /> <GitPullRequest className="size-5 text-gray-400" />
<Link <Link
className="hover:text-blue-500/95 hover:underline" className="hover:text-blue-500/95 hover:underline"
target="_blank" target="_blank"
href={previewDeployment.pullRequestURL} href={previewDeployment.pullRequestURL}
> >
{previewDeployment.pullRequestTitle} {previewDeployment.pullRequestTitle}
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
<div className="justify-center flex-wrap md:p-6 p-2 md:pt-0"> <div className="justify-center flex-wrap md:p-6 p-2 md:pt-0">
<div className="flex flex-wrap justify-end gap-2"> <div className="flex flex-wrap justify-end gap-2">
<ShowModalLogs <ShowModalLogs
appName={previewDeployment.appName} appName={previewDeployment.appName}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button <Button
className="sm:w-auto w-full" className="sm:w-auto w-full"
variant="outline" variant="outline"
size="sm" size="sm"
> >
View Logs View Logs
</Button> </Button>
</ShowModalLogs> </ShowModalLogs>
<ShowPreviewBuilds <ShowPreviewBuilds
deployments={previewDeployment.deployments || []} deployments={previewDeployment.deployments || []}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
/> />
<DialogAction <DialogAction
title="Delete Preview" title="Delete Preview"
description="Are you sure you want to delete this preview?" description="Are you sure you want to delete this preview?"
onClick={() => onClick={() =>
handleDeletePreviewDeployment( handleDeletePreviewDeployment(
previewDeployment.previewDeploymentId previewDeployment.previewDeploymentId,
) )
} }
> >
<Button <Button
className="sm:w-auto w-full" className="sm:w-auto w-full"
variant="destructive" variant="destructive"
isLoading={isLoading} isLoading={isLoading}
size="sm" size="sm"
> >
Delete Preview Delete Preview
</Button> </Button>
</DialogAction> </DialogAction>
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
</> </>
) : ( ) : (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10"> <div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<RocketIcon className="size-8 text-muted-foreground" /> <RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
Preview deployments are disabled for this application, please Preview deployments are disabled for this application, please
enable it enable it
</span> </span>
<ShowPreviewSettings applicationId={applicationId} /> <ShowPreviewSettings applicationId={applicationId} />
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
); );
}; };

View File

@@ -1,5 +1,3 @@
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -20,12 +18,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Secrets } from "@/components/ui/secrets"; import { Secrets } from "@/components/ui/secrets";
import { toast } from "sonner";
import { Switch } from "@/components/ui/switch";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -33,6 +26,13 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const schema = z.object({ const schema = z.object({
env: z.string(), env: z.string(),

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -5,12 +6,10 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../../docker/logs/terminal-line"; import { TerminalLine } from "../../docker/logs/terminal-line";
import { LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { Badge } from "@/components/ui/badge";
import { Loader2 } from "lucide-react";
interface Props { interface Props {
logPath: string | null; logPath: string | null;
@@ -32,19 +31,18 @@ export const ShowDeploymentCompose = ({
const scrollToBottom = () => { const scrollToBottom = () => {
if (autoScroll && scrollRef.current) { if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
} }
}; };
const handleScroll = () => { const handleScroll = () => {
if (!scrollRef.current) return; if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom); setAutoScroll(isAtBottom);
}; };
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
@@ -76,7 +74,6 @@ export const ShowDeploymentCompose = ({
}; };
}, [logPath, open]); }, [logPath, open]);
useEffect(() => { useEffect(() => {
const logs = parseLogs(data); const logs = parseLogs(data);
setFilteredLogs(logs); setFilteredLogs(logs);
@@ -84,11 +81,11 @@ export const ShowDeploymentCompose = ({
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
if (autoScroll && scrollRef.current) { if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
} }
}, [filteredLogs, autoScroll]); }, [filteredLogs, autoScroll]);
return ( return (
<Dialog <Dialog
@@ -110,31 +107,27 @@ export const ShowDeploymentCompose = ({
<DialogHeader> <DialogHeader>
<DialogTitle>Deployment</DialogTitle> <DialogTitle>Deployment</DialogTitle>
<DialogDescription> <DialogDescription>
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge> See all the details of this deployment |{" "}
<Badge variant="blank" className="text-xs">
{filteredLogs.length} lines
</Badge>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div <div
ref={scrollRef} ref={scrollRef}
onScroll={handleScroll} onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar" className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
> >
{filteredLogs.length > 0 ? (
filteredLogs.map((log: LogLine, index: number) => (
{ <TerminalLine key={index} log={log} noTimestamp />
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( ))
<TerminalLine ) : (
key={index}
log={log}
noTimestamp
/>
)) :
(
<div className="flex justify-center items-center h-full text-muted-foreground"> <div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
</div> </div>
) )}
}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -53,7 +53,7 @@ export const DeployCompose = ({ composeId }: Props) => {
}) })
.then(async () => { .then(async () => {
router.push( router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments` `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
); );
}) })
.catch(() => { .catch(() => {

View File

@@ -59,7 +59,7 @@ export const DockerTerminalModal = ({
{children} {children}
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="max-h-screen overflow-y-auto sm:max-w-7xl" className="max-h-screen overflow-y-auto sm:max-w-7xl"
onEscapeKeyDown={(event) => event.preventDefault()} onEscapeKeyDown={(event) => event.preventDefault()}
> >

View File

@@ -1,35 +1,35 @@
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
AlertTriangle, AlertTriangle,
BookIcon, BookIcon,
ExternalLink, ExternalLink,
ExternalLinkIcon, ExternalLinkIcon,
FolderInput, FolderInput,
MoreHorizontalIcon, MoreHorizontalIcon,
TrashIcon, TrashIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Fragment } from "react"; import { Fragment } from "react";
@@ -38,257 +38,257 @@ import { ProjectEnviroment } from "./project-enviroment";
import { UpdateProject } from "./update"; import { UpdateProject } from "./update";
export const ShowProjects = () => { export const ShowProjects = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data } = api.project.all.useQuery(); const { data } = api.project.all.useQuery();
const { data: auth } = api.auth.get.useQuery(); const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery( const { data: user } = api.user.byAuthId.useQuery(
{ {
authId: auth?.id || "", authId: auth?.id || "",
}, },
{ {
enabled: !!auth?.id && auth?.rol === "user", enabled: !!auth?.id && auth?.rol === "user",
} },
); );
const { mutateAsync } = api.project.remove.useMutation(); const { mutateAsync } = api.project.remove.useMutation();
return ( return (
<> <>
{data?.length === 0 && ( {data?.length === 0 && (
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4"> <div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
<FolderInput className="size-10 md:size-28 text-muted-foreground" /> <FolderInput className="size-10 md:size-28 text-muted-foreground" />
<span className="text-center font-medium text-muted-foreground"> <span className="text-center font-medium text-muted-foreground">
No projects added yet. Click on Create project. No projects added yet. Click on Create project.
</span> </span>
</div> </div>
)} )}
<div className="mt-6 w-full grid sm:grid-cols-2 lg:grid-cols-3 flex-wrap gap-5 pb-10"> <div className="mt-6 w-full grid sm:grid-cols-2 lg:grid-cols-3 flex-wrap gap-5 pb-10">
{data?.map((project) => { {data?.map((project) => {
const emptyServices = const emptyServices =
project?.mariadb.length === 0 && project?.mariadb.length === 0 &&
project?.mongo.length === 0 && project?.mongo.length === 0 &&
project?.mysql.length === 0 && project?.mysql.length === 0 &&
project?.postgres.length === 0 && project?.postgres.length === 0 &&
project?.redis.length === 0 && project?.redis.length === 0 &&
project?.applications.length === 0 && project?.applications.length === 0 &&
project?.compose.length === 0; project?.compose.length === 0;
const totalServices = const totalServices =
project?.mariadb.length + project?.mariadb.length +
project?.mongo.length + project?.mongo.length +
project?.mysql.length + project?.mysql.length +
project?.postgres.length + project?.postgres.length +
project?.redis.length + project?.redis.length +
project?.applications.length + project?.applications.length +
project?.compose.length; project?.compose.length;
const flattedDomains = [ const flattedDomains = [
...project.applications.flatMap((a) => a.domains), ...project.applications.flatMap((a) => a.domains),
...project.compose.flatMap((a) => a.domains), ...project.compose.flatMap((a) => a.domains),
]; ];
const renderDomainsDropdown = ( const renderDomainsDropdown = (
item: typeof project.compose | typeof project.applications item: typeof project.compose | typeof project.applications,
) => ) =>
item[0] ? ( item[0] ? (
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuLabel> <DropdownMenuLabel>
{"applicationId" in item[0] ? "Applications" : "Compose"} {"applicationId" in item[0] ? "Applications" : "Compose"}
</DropdownMenuLabel> </DropdownMenuLabel>
{item.map((a) => ( {item.map((a) => (
<Fragment <Fragment
key={"applicationId" in a ? a.applicationId : a.composeId} key={"applicationId" in a ? a.applicationId : a.composeId}
> >
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs "> <DropdownMenuLabel className="font-normal capitalize text-xs ">
{a.name} {a.name}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{a.domains.map((domain) => ( {a.domains.map((domain) => (
<DropdownMenuItem key={domain.domainId} asChild> <DropdownMenuItem key={domain.domainId} asChild>
<Link <Link
className="space-x-4 text-xs cursor-pointer justify-between" className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank" target="_blank"
href={`${domain.https ? "https" : "http"}://${ href={`${domain.https ? "https" : "http"}://${
domain.host domain.host
}${domain.path}`} }${domain.path}`}
> >
<span>{domain.host}</span> <span>{domain.host}</span>
<ExternalLink className="size-4 shrink-0" /> <ExternalLink className="size-4 shrink-0" />
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuGroup> </DropdownMenuGroup>
</Fragment> </Fragment>
))} ))}
</DropdownMenuGroup> </DropdownMenuGroup>
) : null; ) : null;
return ( return (
<div key={project.projectId} className="w-full lg:max-w-md"> <div key={project.projectId} className="w-full lg:max-w-md">
<Link href={`/dashboard/project/${project.projectId}`}> <Link href={`/dashboard/project/${project.projectId}`}>
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card"> <Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
{flattedDomains.length > 1 ? ( {flattedDomains.length > 1 ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <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" 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" size="sm"
variant="default" variant="default"
> >
<ExternalLinkIcon className="size-3.5" /> <ExternalLinkIcon className="size-3.5" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className="w-[200px] space-y-2" className="w-[200px] space-y-2"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{renderDomainsDropdown(project.applications)} {renderDomainsDropdown(project.applications)}
{renderDomainsDropdown(project.compose)} {renderDomainsDropdown(project.compose)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : flattedDomains[0] ? ( ) : flattedDomains[0] ? (
<Button <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" 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" size="sm"
variant="default" variant="default"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Link <Link
href={`${ href={`${
flattedDomains[0].https ? "https" : "http" flattedDomains[0].https ? "https" : "http"
}://${flattedDomains[0].host}${flattedDomains[0].path}`} }://${flattedDomains[0].host}${flattedDomains[0].path}`}
target="_blank" target="_blank"
> >
<ExternalLinkIcon className="size-3.5" /> <ExternalLinkIcon className="size-3.5" />
</Link> </Link>
</Button> </Button>
) : null} ) : null}
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between gap-2"> <CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5"> <span className="flex flex-col gap-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookIcon className="size-4 text-muted-foreground" /> <BookIcon className="size-4 text-muted-foreground" />
<span className="text-base font-medium leading-none"> <span className="text-base font-medium leading-none">
{project.name} {project.name}
</span> </span>
</div> </div>
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">
{project.description} {project.description}
</span> </span>
</span> </span>
<div className="flex self-start space-x-1"> <div className="flex self-start space-x-1">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="px-2" className="px-2"
> >
<MoreHorizontalIcon className="size-5" /> <MoreHorizontalIcon className="size-5" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px] space-y-2"> <DropdownMenuContent className="w-[200px] space-y-2">
<DropdownMenuLabel className="font-normal"> <DropdownMenuLabel className="font-normal">
Actions Actions
</DropdownMenuLabel> </DropdownMenuLabel>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<ProjectEnviroment <ProjectEnviroment
projectId={project.projectId} projectId={project.projectId}
/> />
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<UpdateProject projectId={project.projectId} /> <UpdateProject projectId={project.projectId} />
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
{(auth?.rol === "admin" || {(auth?.rol === "admin" ||
user?.canDeleteProjects) && ( user?.canDeleteProjects) && (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger className="w-full"> <AlertDialogTrigger className="w-full">
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer space-x-3" className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()} onSelect={(e) => e.preventDefault()}
> >
<TrashIcon className="size-4" /> <TrashIcon className="size-4" />
<span>Delete</span> <span>Delete</span>
</DropdownMenuItem> </DropdownMenuItem>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
Are you sure to delete this project? Are you sure to delete this project?
</AlertDialogTitle> </AlertDialogTitle>
{!emptyServices ? ( {!emptyServices ? (
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950"> <div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" /> <AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400"> <span className="text-sm text-yellow-600 dark:text-yellow-400">
You have active services, please You have active services, please
delete them first delete them first
</span> </span>
</div> </div>
) : ( ) : (
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone This action cannot be undone
</AlertDialogDescription> </AlertDialogDescription>
)} )}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel> <AlertDialogCancel>
Cancel Cancel
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
disabled={!emptyServices} disabled={!emptyServices}
onClick={async () => { onClick={async () => {
await mutateAsync({ await mutateAsync({
projectId: project.projectId, projectId: project.projectId,
}) })
.then(() => { .then(() => {
toast.success( toast.success(
"Project delete succesfully" "Project delete succesfully",
); );
}) })
.catch(() => { .catch(() => {
toast.error( toast.error(
"Error to delete this project" "Error to delete this project",
); );
}) })
.finally(() => { .finally(() => {
utils.project.all.invalidate(); utils.project.all.invalidate();
}); });
}} }}
> >
Delete Delete
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
)} )}
</div> </div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardFooter className="pt-4"> <CardFooter className="pt-4">
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4"> <div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}> <DateTooltip date={project.createdAt}>
Created Created
</DateTooltip> </DateTooltip>
<span> <span>
{totalServices}{" "} {totalServices}{" "}
{totalServices === 1 ? "service" : "services"} {totalServices === 1 ? "service" : "services"}
</span> </span>
</div> </div>
</CardFooter> </CardFooter>
</Card> </Card>
</Link> </Link>
</div> </div>
); );
})} })}
</div> </div>
</> </>
); );
}; };

View File

@@ -1,189 +1,189 @@
"use client"; "use client";
import React from "react";
import { import {
Command, MariadbIcon,
CommandEmpty, MongodbIcon,
CommandList, MysqlIcon,
CommandGroup, PostgresqlIcon,
CommandInput, RedisIcon,
CommandItem, } from "@/components/icons/data-tools-icons";
CommandDialog, import { Badge } from "@/components/ui/badge";
CommandSeparator, import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { useRouter } from "next/router";
import { import {
extractServices, type Services,
type Services, extractServices,
} from "@/pages/dashboard/project/[projectId]"; } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
import type { findProjectById } from "@dokploy/server/services/project"; import type { findProjectById } from "@dokploy/server/services/project";
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react"; import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
import { import { useRouter } from "next/router";
MariadbIcon, import React from "react";
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
RedisIcon,
} from "@/components/icons/data-tools-icons";
import { api } from "@/utils/api";
import { Badge } from "@/components/ui/badge";
import { StatusTooltip } from "../shared/status-tooltip"; import { StatusTooltip } from "../shared/status-tooltip";
type Project = Awaited<ReturnType<typeof findProjectById>>; type Project = Awaited<ReturnType<typeof findProjectById>>;
export const SearchCommand = () => { export const SearchCommand = () => {
const router = useRouter(); const router = useRouter();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState("");
const { data } = api.project.all.useQuery(); const { data } = api.project.all.useQuery();
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
React.useEffect(() => { React.useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) { if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault();
setOpen((open) => !open); setOpen((open) => !open);
} }
}; };
document.addEventListener("keydown", down); document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down); return () => document.removeEventListener("keydown", down);
}, []); }, []);
return ( return (
<div> <div>
<CommandDialog open={open} onOpenChange={setOpen}> <CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput <CommandInput
placeholder={"Search projects or settings"} placeholder={"Search projects or settings"}
value={search} value={search}
onValueChange={setSearch} onValueChange={setSearch}
/> />
<CommandList> <CommandList>
<CommandEmpty> <CommandEmpty>
No projects added yet. Click on Create project. No projects added yet. Click on Create project.
</CommandEmpty> </CommandEmpty>
<CommandGroup heading={"Projects"}> <CommandGroup heading={"Projects"}>
<CommandList> <CommandList>
{data?.map((project) => ( {data?.map((project) => (
<CommandItem <CommandItem
key={project.projectId} key={project.projectId}
onSelect={() => { onSelect={() => {
router.push(`/dashboard/project/${project.projectId}`); router.push(`/dashboard/project/${project.projectId}`);
setOpen(false); setOpen(false);
}} }}
> >
<BookIcon className="size-4 text-muted-foreground mr-2" /> <BookIcon className="size-4 text-muted-foreground mr-2" />
{project.name} {project.name}
</CommandItem> </CommandItem>
))} ))}
</CommandList> </CommandList>
</CommandGroup> </CommandGroup>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading={"Services"}> <CommandGroup heading={"Services"}>
<CommandList> <CommandList>
{data?.map((project) => { {data?.map((project) => {
const applications: Services[] = extractServices(project); const applications: Services[] = extractServices(project);
return applications.map((application) => ( return applications.map((application) => (
<CommandItem <CommandItem
key={application.id} key={application.id}
onSelect={() => { onSelect={() => {
router.push( router.push(
`/dashboard/project/${project.projectId}/services/${application.type}/${application.id}` `/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`,
); );
setOpen(false); setOpen(false);
}} }}
> >
{application.type === "postgres" && ( {application.type === "postgres" && (
<PostgresqlIcon className="h-6 w-6 mr-2" /> <PostgresqlIcon className="h-6 w-6 mr-2" />
)} )}
{application.type === "redis" && ( {application.type === "redis" && (
<RedisIcon className="h-6 w-6 mr-2" /> <RedisIcon className="h-6 w-6 mr-2" />
)} )}
{application.type === "mariadb" && ( {application.type === "mariadb" && (
<MariadbIcon className="h-6 w-6 mr-2" /> <MariadbIcon className="h-6 w-6 mr-2" />
)} )}
{application.type === "mongo" && ( {application.type === "mongo" && (
<MongodbIcon className="h-6 w-6 mr-2" /> <MongodbIcon className="h-6 w-6 mr-2" />
)} )}
{application.type === "mysql" && ( {application.type === "mysql" && (
<MysqlIcon className="h-6 w-6 mr-2" /> <MysqlIcon className="h-6 w-6 mr-2" />
)} )}
{application.type === "application" && ( {application.type === "application" && (
<GlobeIcon className="h-6 w-6 mr-2" /> <GlobeIcon className="h-6 w-6 mr-2" />
)} )}
{application.type === "compose" && ( {application.type === "compose" && (
<CircuitBoard className="h-6 w-6 mr-2" /> <CircuitBoard className="h-6 w-6 mr-2" />
)} )}
<span className="flex-grow"> <span className="flex-grow">
{project.name} / {application.name}{" "} {project.name} / {application.name}{" "}
<div style={{ display: "none" }}>{application.id}</div> <div style={{ display: "none" }}>{application.id}</div>
</span> </span>
<div> <div>
<StatusTooltip status={application.status} /> <StatusTooltip status={application.status} />
</div> </div>
</CommandItem> </CommandItem>
)); ));
})} })}
</CommandList> </CommandList>
</CommandGroup> </CommandGroup>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading={"Application"} hidden={true}> <CommandGroup heading={"Application"} hidden={true}>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/projects"); router.push("/dashboard/projects");
setOpen(false); setOpen(false);
}} }}
> >
Projects Projects
</CommandItem> </CommandItem>
{!isCloud && ( {!isCloud && (
<> <>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/monitoring"); router.push("/dashboard/monitoring");
setOpen(false); setOpen(false);
}} }}
> >
Monitoring Monitoring
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/traefik"); router.push("/dashboard/traefik");
setOpen(false); setOpen(false);
}} }}
> >
Traefik Traefik
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/docker"); router.push("/dashboard/docker");
setOpen(false); setOpen(false);
}} }}
> >
Docker Docker
</CommandItem> </CommandItem>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/requests"); router.push("/dashboard/requests");
setOpen(false); setOpen(false);
}} }}
> >
Requests Requests
</CommandItem> </CommandItem>
</> </>
)} )}
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
router.push("/dashboard/settings/server"); router.push("/dashboard/settings/server");
setOpen(false); setOpen(false);
}} }}
> >
Settings Settings
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
</div> </div>
); );
}; };

View File

@@ -24,12 +24,12 @@ export const DeleteNotification = ({ notificationId }: Props) => {
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-9 w-9 group hover:bg-red-500/10" className="h-9 w-9 group hover:bg-red-500/10"
isLoading={isLoading} isLoading={isLoading}
> >
<Trash2 className="size-4 text-muted-foreground group-hover:text-red-500" /> <Trash2 className="size-4 text-muted-foreground group-hover:text-red-500" />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>

View File

@@ -40,58 +40,60 @@ export const ShowNotifications = () => {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="grid lg:grid-cols-1 xl:grid-cols-2 gap-4"> <div className="grid lg:grid-cols-1 xl:grid-cols-2 gap-4">
{data?.map((notification, index) => ( {data?.map((notification, index) => (
<div <div
key={notification.notificationId} key={notification.notificationId}
className="flex items-center justify-between rounded-xl p-4 transition-colors dark:bg-zinc-900/50 bg-gray-200/50 border border-card" className="flex items-center justify-between rounded-xl p-4 transition-colors dark:bg-zinc-900/50 bg-gray-200/50 border border-card"
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{notification.notificationType === "slack" && ( {notification.notificationType === "slack" && (
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10"> <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10">
<SlackIcon className="h-6 w-6 text-indigo-400" /> <SlackIcon className="h-6 w-6 text-indigo-400" />
</div> </div>
)} )}
{notification.notificationType === "telegram" && ( {notification.notificationType === "telegram" && (
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-cyan-500/10"> <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-cyan-500/10">
<TelegramIcon className="h-6 w-6 text-indigo-400" /> <TelegramIcon className="h-6 w-6 text-indigo-400" />
</div> </div>
)} )}
{notification.notificationType === "discord" && ( {notification.notificationType === "discord" && (
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10"> <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10">
<DiscordIcon className="h-6 w-6 text-indigo-400" /> <DiscordIcon className="h-6 w-6 text-indigo-400" />
</div> </div>
)} )}
{notification.notificationType === "email" && ( {notification.notificationType === "email" && (
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-500/10"> <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-500/10">
<Mail className="h-6 w-6 text-indigo-400" /> <Mail className="h-6 w-6 text-indigo-400" />
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium dark:text-zinc-300 text-zinc-800"> <span className="text-sm font-medium dark:text-zinc-300 text-zinc-800">
{notification.name} {notification.name}
</span> </span>
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
{notification.notificationType?.[0]?.toUpperCase() + notification.notificationType?.slice(1)} notification {notification.notificationType?.[0]?.toUpperCase() +
</span> notification.notificationType?.slice(1)}{" "}
</div> notification
</div> </span>
<div className="flex items-center gap-2"> </div>
<UpdateNotification </div>
notificationId={notification.notificationId} <div className="flex items-center gap-2">
/> <UpdateNotification
<DeleteNotification notificationId={notification.notificationId}
notificationId={notification.notificationId} />
/> <DeleteNotification
</div> notificationId={notification.notificationId}
/>
</div>
</div>
))}
</div>
<div className="flex flex-col gap-4 justify-end w-full items-end">
<AddNotification />
</div> </div>
))}
</div> </div>
<div className="flex flex-col gap-4 justify-end w-full items-end">
<AddNotification />
</div>
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -218,9 +218,11 @@ export const UpdateNotification = ({ notificationId }: Props) => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<Button variant="ghost" <Button
size="icon" variant="ghost"
className="h-9 w-9 dark:hover:bg-zinc-900/80 hover:bg-gray-200/80"> size="icon"
className="h-9 w-9 dark:hover:bg-zinc-900/80 hover:bg-gray-200/80"
>
<Pen className="size-4 text-muted-foreground" /> <Pen className="size-4 text-muted-foreground" />
</Button> </Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -26,7 +27,6 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Disable2FA } from "./disable-2fa"; import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa"; import { Enable2FA } from "./enable-2fa";
import { AlertBlock } from "@/components/shared/alert-block";
const profileSchema = z.object({ const profileSchema = z.object({
email: z.string(), email: z.string(),

View File

@@ -1,3 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -18,13 +20,11 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { DialogAction } from "@/components/shared/dialog-action";
import { AlertBlock } from "@/components/shared/alert-block";
import { useRouter } from "next/router";
const profileSchema = z.object({ const profileSchema = z.object({
password: z.string().min(1, { password: z.string().min(1, {

View File

@@ -25,8 +25,8 @@ import { toast } from "sonner";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
interface Props { interface Props {
serverId?: string; serverId?: string;

View File

@@ -108,7 +108,8 @@ export const EditScript = ({ serverId }: Props) => {
</DialogDescription> </DialogDescription>
<AlertBlock type="warning"> <AlertBlock type="warning">
We recommend not modifying this script unless you know what you are doing. We recommend not modifying this script unless you know what you are
doing.
</AlertBlock> </AlertBlock>
</DialogHeader> </DialogHeader>
<div className="grid gap-4"> <div className="grid gap-4">

View File

@@ -34,8 +34,8 @@ import { toast } from "sonner";
import { ShowDeployment } from "../../application/deployments/show-deployment"; import { ShowDeployment } from "../../application/deployments/show-deployment";
import { EditScript } from "./edit-script"; import { EditScript } from "./edit-script";
import { GPUSupport } from "./gpu-support"; import { GPUSupport } from "./gpu-support";
import { ValidateServer } from "./validate-server";
import { SecurityAudit } from "./security-audit"; import { SecurityAudit } from "./security-audit";
import { ValidateServer } from "./validate-server";
interface Props { interface Props {
serverId: string; serverId: string;

View File

@@ -23,17 +23,17 @@ import { api } from "@/utils/api";
import { format } from "date-fns"; import { format } from "date-fns";
import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react"; import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner"; import { toast } from "sonner";
import { TerminalModal } from "../web-server/terminal-modal"; import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions"; import { ShowServerActions } from "./actions/show-server-actions";
import { AddServer } from "./add-server"; import { AddServer } from "./add-server";
import { SetupServer } from "./setup-server"; import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal"; import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { UpdateServer } from "./update-server"; import { UpdateServer } from "./update-server";
import { useRouter } from "next/router";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
export const ShowServers = () => { export const ShowServers = () => {
const router = useRouter(); const router = useRouter();

View File

@@ -1,12 +1,12 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ExternalLinkIcon, Loader2 } from "lucide-react";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { ExternalLinkIcon, Loader2 } from "lucide-react";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor";
import Link from "next/link";
export const CreateSSHKey = () => { export const CreateSSHKey = () => {
const { data, refetch } = api.sshKey.all.useQuery(); const { data, refetch } = api.sshKey.all.useQuery();

View File

@@ -5,26 +5,26 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent,
CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { RocketIcon } from "lucide-react";
import { toast } from "sonner";
import { EditScript } from "../edit-script";
import { api } from "@/utils/api";
import { useState } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
SelectTrigger,
SelectValue,
SelectContent, SelectContent,
SelectGroup, SelectGroup,
SelectItem, SelectItem,
SelectLabel, SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { EditScript } from "../edit-script";
export const Setup = () => { export const Setup = () => {
const { data: servers } = api.server.all.useQuery(); const { data: servers } = api.server.all.useQuery();

View File

@@ -1,27 +1,27 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent,
CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Loader2, PcCase, RefreshCw } from "lucide-react";
import { api } from "@/utils/api";
import { useState } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { Loader2, PcCase, RefreshCw } from "lucide-react";
import { useState } from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Select, Select,
SelectTrigger,
SelectValue,
SelectContent, SelectContent,
SelectGroup, SelectGroup,
SelectItem, SelectItem,
SelectLabel, SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { StatusRow } from "../gpu-support"; import { StatusRow } from "../gpu-support";
import { AlertBlock } from "@/components/shared/alert-block";
export const Verify = () => { export const Verify = () => {
const { data: servers } = api.server.all.useQuery(); const { data: servers } = api.server.all.useQuery();

View File

@@ -1,3 +1,5 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -7,21 +9,19 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { defineStepper } from "@stepperize/react";
import { BookIcon, Puzzle } from "lucide-react"; import { BookIcon, Puzzle } from "lucide-react";
import { Code2, Database, GitMerge, Globe, Plug, Users } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { defineStepper } from "@stepperize/react";
import React from "react"; import React from "react";
import { Separator } from "@/components/ui/separator"; import ConfettiExplosion from "react-confetti-explosion";
import { AlertBlock } from "@/components/shared/alert-block";
import { CreateServer } from "./create-server"; import { CreateServer } from "./create-server";
import { CreateSSHKey } from "./create-ssh-key"; import { CreateSSHKey } from "./create-ssh-key";
import { Setup } from "./setup"; import { Setup } from "./setup";
import { Verify } from "./verify"; import { Verify } from "./verify";
import { Database, Globe, GitMerge, Users, Code2, Plug } from "lucide-react";
import ConfettiExplosion from "react-confetti-explosion";
import Link from "next/link";
import { GithubIcon } from "@/components/icons/data-tools-icons";
export const { useStepper, steps, Scoped } = defineStepper( export const { useStepper, steps, Scoped } = defineStepper(
{ {

View File

@@ -80,7 +80,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
return ( return (
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}> <Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
className="max-h-[85vh] overflow-y-auto sm:max-w-7xl" className="max-h-[85vh] overflow-y-auto sm:max-w-7xl"
onEscapeKeyDown={(event) => event.preventDefault()} onEscapeKeyDown={(event) => event.preventDefault()}
> >

View File

@@ -6,6 +6,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -13,7 +14,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import type React from "react"; import type React from "react";

View File

@@ -3,19 +3,19 @@ import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types"; import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { generateRandomDomain } from "@/templates/utils";
import { import {
createPreviewDeployment,
type Domain, type Domain,
IS_CLOUD,
createPreviewDeployment,
findPreviewDeploymentByApplicationId, findPreviewDeploymentByApplicationId,
findPreviewDeploymentsByPullRequestId, findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment, removePreviewDeployment,
} from "@dokploy/server"; } from "@dokploy/server";
import { Webhooks } from "@octokit/webhooks"; import { Webhooks } from "@octokit/webhooks";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { extractCommitMessage, extractHash } from "./[refreshToken]"; import { extractCommitMessage, extractHash } from "./[refreshToken]";
import { generateRandomDomain } from "@/templates/utils";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,

View File

@@ -1 +1 @@
{} {}

View File

@@ -1,44 +1,44 @@
{ {
"settings.common.save": "Salva", "settings.common.save": "Salva",
"settings.server.domain.title": "Dominio del server", "settings.server.domain.title": "Dominio del server",
"settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.", "settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.",
"settings.server.domain.form.domain": "Dominio", "settings.server.domain.form.domain": "Dominio",
"settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt", "settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificato", "settings.server.domain.form.certificate.label": "Certificato",
"settings.server.domain.form.certificate.placeholder": "Seleziona un certificato", "settings.server.domain.form.certificate.placeholder": "Seleziona un certificato",
"settings.server.domain.form.certificateOptions.none": "Nessuno", "settings.server.domain.form.certificateOptions.none": "Nessuno",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)", "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)",
"settings.server.webServer.title": "Server Web", "settings.server.webServer.title": "Server Web",
"settings.server.webServer.description": "Ricarica o pulisci il server web.", "settings.server.webServer.description": "Ricarica o pulisci il server web.",
"settings.server.webServer.actions": "Azioni", "settings.server.webServer.actions": "Azioni",
"settings.server.webServer.reload": "Ricarica", "settings.server.webServer.reload": "Ricarica",
"settings.server.webServer.watchLogs": "Guarda i log", "settings.server.webServer.watchLogs": "Guarda i log",
"settings.server.webServer.updateServerIp": "Aggiorna IP del server", "settings.server.webServer.updateServerIp": "Aggiorna IP del server",
"settings.server.webServer.server.label": "Server", "settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modifica Env", "settings.server.webServer.traefik.modifyEnv": "Modifica Env",
"settings.server.webServer.storage.label": "Spazio", "settings.server.webServer.storage.label": "Spazio",
"settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate", "settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate",
"settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati", "settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati",
"settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati", "settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati",
"settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema", "settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema",
"settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio", "settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio",
"settings.server.webServer.storage.cleanAll": "Pulisci tutto", "settings.server.webServer.storage.cleanAll": "Pulisci tutto",
"settings.profile.title": "Account", "settings.profile.title": "Account",
"settings.profile.description": "Modifica i dettagli del tuo profilo qui.", "settings.profile.description": "Modifica i dettagli del tuo profilo qui.",
"settings.profile.email": "Email", "settings.profile.email": "Email",
"settings.profile.password": "Password", "settings.profile.password": "Password",
"settings.profile.avatar": "Avatar", "settings.profile.avatar": "Avatar",
"settings.appearance.title": "Aspetto", "settings.appearance.title": "Aspetto",
"settings.appearance.description": "Personalizza il tema della tua dashboard.", "settings.appearance.description": "Personalizza il tema della tua dashboard.",
"settings.appearance.theme": "Tema", "settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard", "settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard",
"settings.appearance.themes.light": "Chiaro", "settings.appearance.themes.light": "Chiaro",
"settings.appearance.themes.dark": "Scuro", "settings.appearance.themes.dark": "Scuro",
"settings.appearance.themes.system": "Sistema", "settings.appearance.themes.system": "Sistema",
"settings.appearance.language": "Lingua", "settings.appearance.language": "Lingua",
"settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard" "settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard"
} }

View File

@@ -1,22 +1,22 @@
import { import {
type DomainSchema, type DomainSchema,
type Schema, type Schema,
type Template, type Template,
generateRandomDomain, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const randomDomain = generateRandomDomain(schema); const randomDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [ const domains: DomainSchema[] = [
{ {
host: randomDomain, host: randomDomain,
port: 6610, port: 6610,
serviceName: "onedev", serviceName: "onedev",
}, },
]; ];
return { return {
domains, domains,
}; };
} }

View File

@@ -1,44 +1,44 @@
import { import {
generateHash, type DomainSchema,
generateRandomDomain, type Schema,
generateBase64, type Template,
type Template, generateBase64,
type Schema, generateHash,
type DomainSchema, generateRandomDomain,
} from "../utils"; } from "../utils";
export function generate(schema: Schema): Template { export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema); const mainDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64); const secretBase = generateBase64(64);
const domains: DomainSchema[] = [ const domains: DomainSchema[] = [
{ {
host: mainDomain, host: mainDomain,
port: 3000, port: 3000,
serviceName: "unsend", serviceName: "unsend",
}, },
]; ];
const envs = [ const envs = [
"REDIS_URL=redis://unsend-redis-prod:6379", "REDIS_URL=redis://unsend-redis-prod:6379",
"POSTGRES_USER=postgres", "POSTGRES_USER=postgres",
"POSTGRES_PASSWORD=postgres", "POSTGRES_PASSWORD=postgres",
"POSTGRES_DB=unsend", "POSTGRES_DB=unsend",
"DATABASE_URL=postgresql://postgres:postgres@unsend-db-prod:5432/unsend", "DATABASE_URL=postgresql://postgres:postgres@unsend-db-prod:5432/unsend",
"NEXTAUTH_URL=http://localhost:3000", "NEXTAUTH_URL=http://localhost:3000",
`NEXTAUTH_SECRET=${secretBase}`, `NEXTAUTH_SECRET=${secretBase}`,
"GITHUB_ID='Fill'", "GITHUB_ID='Fill'",
"GITHUB_SECRET='Fill'", "GITHUB_SECRET='Fill'",
"AWS_DEFAULT_REGION=us-east-1", "AWS_DEFAULT_REGION=us-east-1",
"AWS_SECRET_KEY='Fill'", "AWS_SECRET_KEY='Fill'",
"AWS_ACCESS_KEY='Fill'", "AWS_ACCESS_KEY='Fill'",
"DOCKER_OUTPUT=1", "DOCKER_OUTPUT=1",
"API_RATE_LIMIT=1", "API_RATE_LIMIT=1",
"DISCORD_WEBHOOK_URL=", "DISCORD_WEBHOOK_URL=",
]; ];
return { return {
envs, envs,
domains, domains,
}; };
} }

View File

@@ -1,11 +1,11 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
integer, integer,
json, json,
pgEnum, pgEnum,
pgTable, pgTable,
text, text,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -28,493 +28,493 @@ import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const sourceType = pgEnum("sourceType", [ export const sourceType = pgEnum("sourceType", [
"docker", "docker",
"git", "git",
"github", "github",
"gitlab", "gitlab",
"bitbucket", "bitbucket",
"drop", "drop",
]); ]);
export const buildType = pgEnum("buildType", [ export const buildType = pgEnum("buildType", [
"dockerfile", "dockerfile",
"heroku_buildpacks", "heroku_buildpacks",
"paketo_buildpacks", "paketo_buildpacks",
"nixpacks", "nixpacks",
"static", "static",
]); ]);
// TODO: refactor this types // TODO: refactor this types
export interface HealthCheckSwarm { export interface HealthCheckSwarm {
Test?: string[] | undefined; Test?: string[] | undefined;
Interval?: number | undefined; Interval?: number | undefined;
Timeout?: number | undefined; Timeout?: number | undefined;
StartPeriod?: number | undefined; StartPeriod?: number | undefined;
Retries?: number | undefined; Retries?: number | undefined;
} }
export interface RestartPolicySwarm { export interface RestartPolicySwarm {
Condition?: string | undefined; Condition?: string | undefined;
Delay?: number | undefined; Delay?: number | undefined;
MaxAttempts?: number | undefined; MaxAttempts?: number | undefined;
Window?: number | undefined; Window?: number | undefined;
} }
export interface PlacementSwarm { export interface PlacementSwarm {
Constraints?: string[] | undefined; Constraints?: string[] | undefined;
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined; Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
MaxReplicas?: number | undefined; MaxReplicas?: number | undefined;
Platforms?: Platforms?:
| Array<{ | Array<{
Architecture: string; Architecture: string;
OS: string; OS: string;
}> }>
| undefined; | undefined;
} }
export interface UpdateConfigSwarm { export interface UpdateConfigSwarm {
Parallelism: number; Parallelism: number;
Delay?: number | undefined; Delay?: number | undefined;
FailureAction?: string | undefined; FailureAction?: string | undefined;
Monitor?: number | undefined; Monitor?: number | undefined;
MaxFailureRatio?: number | undefined; MaxFailureRatio?: number | undefined;
Order: string; Order: string;
} }
export interface ServiceModeSwarm { export interface ServiceModeSwarm {
Replicated?: { Replicas?: number | undefined } | undefined; Replicated?: { Replicas?: number | undefined } | undefined;
Global?: {} | undefined; Global?: {} | undefined;
ReplicatedJob?: ReplicatedJob?:
| { | {
MaxConcurrent?: number | undefined; MaxConcurrent?: number | undefined;
TotalCompletions?: number | undefined; TotalCompletions?: number | undefined;
} }
| undefined; | undefined;
GlobalJob?: {} | undefined; GlobalJob?: {} | undefined;
} }
export interface NetworkSwarm { export interface NetworkSwarm {
Target?: string | undefined; Target?: string | undefined;
Aliases?: string[] | undefined; Aliases?: string[] | undefined;
DriverOpts?: { [key: string]: string } | undefined; DriverOpts?: { [key: string]: string } | undefined;
} }
export interface LabelsSwarm { export interface LabelsSwarm {
[name: string]: string; [name: string]: string;
} }
export const applications = pgTable("application", { export const applications = pgTable("application", {
applicationId: text("applicationId") applicationId: text("applicationId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
name: text("name").notNull(), name: text("name").notNull(),
appName: text("appName") appName: text("appName")
.notNull() .notNull()
.$defaultFn(() => generateAppName("app")) .$defaultFn(() => generateAppName("app"))
.unique(), .unique(),
description: text("description"), description: text("description"),
env: text("env"), env: text("env"),
previewEnv: text("previewEnv"), previewEnv: text("previewEnv"),
previewBuildArgs: text("previewBuildArgs"), previewBuildArgs: text("previewBuildArgs"),
previewWildcard: text("previewWildcard"), previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000), previewPort: integer("previewPort").default(3000),
previewHttps: boolean("previewHttps").notNull().default(false), previewHttps: boolean("previewHttps").notNull().default(false),
previewPath: text("previewPath").default("/"), previewPath: text("previewPath").default("/"),
previewCertificateType: certificateType("certificateType") previewCertificateType: certificateType("certificateType")
.notNull() .notNull()
.default("none"), .default("none"),
previewLimit: integer("previewLimit").default(3), previewLimit: integer("previewLimit").default(3),
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
false false,
), ),
buildArgs: text("buildArgs"), buildArgs: text("buildArgs"),
memoryReservation: integer("memoryReservation"), memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"), memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"), cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"), cpuLimit: integer("cpuLimit"),
title: text("title"), title: text("title"),
enabled: boolean("enabled"), enabled: boolean("enabled"),
subtitle: text("subtitle"), subtitle: text("subtitle"),
command: text("command"), command: text("command"),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()), refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceType("sourceType").notNull().default("github"), sourceType: sourceType("sourceType").notNull().default("github"),
// Github // Github
repository: text("repository"), repository: text("repository"),
owner: text("owner"), owner: text("owner"),
branch: text("branch"), branch: text("branch"),
buildPath: text("buildPath").default("/"), buildPath: text("buildPath").default("/"),
autoDeploy: boolean("autoDeploy").$defaultFn(() => true), autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
// Gitlab // Gitlab
gitlabProjectId: integer("gitlabProjectId"), gitlabProjectId: integer("gitlabProjectId"),
gitlabRepository: text("gitlabRepository"), gitlabRepository: text("gitlabRepository"),
gitlabOwner: text("gitlabOwner"), gitlabOwner: text("gitlabOwner"),
gitlabBranch: text("gitlabBranch"), gitlabBranch: text("gitlabBranch"),
gitlabBuildPath: text("gitlabBuildPath").default("/"), gitlabBuildPath: text("gitlabBuildPath").default("/"),
gitlabPathNamespace: text("gitlabPathNamespace"), gitlabPathNamespace: text("gitlabPathNamespace"),
// Bitbucket // Bitbucket
bitbucketRepository: text("bitbucketRepository"), bitbucketRepository: text("bitbucketRepository"),
bitbucketOwner: text("bitbucketOwner"), bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"), bitbucketBranch: text("bitbucketBranch"),
bitbucketBuildPath: text("bitbucketBuildPath").default("/"), bitbucketBuildPath: text("bitbucketBuildPath").default("/"),
// Docker // Docker
username: text("username"), username: text("username"),
password: text("password"), password: text("password"),
dockerImage: text("dockerImage"), dockerImage: text("dockerImage"),
registryUrl: text("registryUrl"), registryUrl: text("registryUrl"),
// Git // Git
customGitUrl: text("customGitUrl"), customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"), customGitBranch: text("customGitBranch"),
customGitBuildPath: text("customGitBuildPath"), customGitBuildPath: text("customGitBuildPath"),
customGitSSHKeyId: text("customGitSSHKeyId").references( customGitSSHKeyId: text("customGitSSHKeyId").references(
() => sshKeys.sshKeyId, () => sshKeys.sshKeyId,
{ {
onDelete: "set null", onDelete: "set null",
} },
), ),
dockerfile: text("dockerfile"), dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"), dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"), dockerBuildStage: text("dockerBuildStage"),
// Drop // Drop
dropBuildPath: text("dropBuildPath"), dropBuildPath: text("dropBuildPath"),
// Docker swarm json // Docker swarm json
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(), healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(), restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(), placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(), updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(), rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
// //
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
herokuVersion: text("herokuVersion").default("24"), herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"), publishDirectory: text("publishDirectory"),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
registryId: text("registryId").references(() => registry.registryId, { registryId: text("registryId").references(() => registry.registryId, {
onDelete: "set null", onDelete: "set null",
}), }),
projectId: text("projectId") projectId: text("projectId")
.notNull() .notNull()
.references(() => projects.projectId, { onDelete: "cascade" }), .references(() => projects.projectId, { onDelete: "cascade" }),
githubId: text("githubId").references(() => github.githubId, { githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null", onDelete: "set null",
}), }),
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, { gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
onDelete: "set null", onDelete: "set null",
}), }),
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, { bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
onDelete: "set null", onDelete: "set null",
}), }),
serverId: text("serverId").references(() => server.serverId, { serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
}); });
export const applicationsRelations = relations( export const applicationsRelations = relations(
applications, applications,
({ one, many }) => ({ ({ one, many }) => ({
project: one(projects, { project: one(projects, {
fields: [applications.projectId], fields: [applications.projectId],
references: [projects.projectId], references: [projects.projectId],
}), }),
deployments: many(deployments), deployments: many(deployments),
customGitSSHKey: one(sshKeys, { customGitSSHKey: one(sshKeys, {
fields: [applications.customGitSSHKeyId], fields: [applications.customGitSSHKeyId],
references: [sshKeys.sshKeyId], references: [sshKeys.sshKeyId],
}), }),
domains: many(domains), domains: many(domains),
mounts: many(mounts), mounts: many(mounts),
redirects: many(redirects), redirects: many(redirects),
security: many(security), security: many(security),
ports: many(ports), ports: many(ports),
registry: one(registry, { registry: one(registry, {
fields: [applications.registryId], fields: [applications.registryId],
references: [registry.registryId], references: [registry.registryId],
}), }),
github: one(github, { github: one(github, {
fields: [applications.githubId], fields: [applications.githubId],
references: [github.githubId], references: [github.githubId],
}), }),
gitlab: one(gitlab, { gitlab: one(gitlab, {
fields: [applications.gitlabId], fields: [applications.gitlabId],
references: [gitlab.gitlabId], references: [gitlab.gitlabId],
}), }),
bitbucket: one(bitbucket, { bitbucket: one(bitbucket, {
fields: [applications.bitbucketId], fields: [applications.bitbucketId],
references: [bitbucket.bitbucketId], references: [bitbucket.bitbucketId],
}), }),
server: one(server, { server: one(server, {
fields: [applications.serverId], fields: [applications.serverId],
references: [server.serverId], references: [server.serverId],
}), }),
previewDeployments: many(previewDeployments), previewDeployments: many(previewDeployments),
}) }),
); );
const HealthCheckSwarmSchema = z const HealthCheckSwarmSchema = z
.object({ .object({
Test: z.array(z.string()).optional(), Test: z.array(z.string()).optional(),
Interval: z.number().optional(), Interval: z.number().optional(),
Timeout: z.number().optional(), Timeout: z.number().optional(),
StartPeriod: z.number().optional(), StartPeriod: z.number().optional(),
Retries: z.number().optional(), Retries: z.number().optional(),
}) })
.strict(); .strict();
const RestartPolicySwarmSchema = z const RestartPolicySwarmSchema = z
.object({ .object({
Condition: z.string().optional(), Condition: z.string().optional(),
Delay: z.number().optional(), Delay: z.number().optional(),
MaxAttempts: z.number().optional(), MaxAttempts: z.number().optional(),
Window: z.number().optional(), Window: z.number().optional(),
}) })
.strict(); .strict();
const PreferenceSchema = z const PreferenceSchema = z
.object({ .object({
Spread: z.object({ Spread: z.object({
SpreadDescriptor: z.string(), SpreadDescriptor: z.string(),
}), }),
}) })
.strict(); .strict();
const PlatformSchema = z const PlatformSchema = z
.object({ .object({
Architecture: z.string(), Architecture: z.string(),
OS: z.string(), OS: z.string(),
}) })
.strict(); .strict();
const PlacementSwarmSchema = z const PlacementSwarmSchema = z
.object({ .object({
Constraints: z.array(z.string()).optional(), Constraints: z.array(z.string()).optional(),
Preferences: z.array(PreferenceSchema).optional(), Preferences: z.array(PreferenceSchema).optional(),
MaxReplicas: z.number().optional(), MaxReplicas: z.number().optional(),
Platforms: z.array(PlatformSchema).optional(), Platforms: z.array(PlatformSchema).optional(),
}) })
.strict(); .strict();
const UpdateConfigSwarmSchema = z const UpdateConfigSwarmSchema = z
.object({ .object({
Parallelism: z.number(), Parallelism: z.number(),
Delay: z.number().optional(), Delay: z.number().optional(),
FailureAction: z.string().optional(), FailureAction: z.string().optional(),
Monitor: z.number().optional(), Monitor: z.number().optional(),
MaxFailureRatio: z.number().optional(), MaxFailureRatio: z.number().optional(),
Order: z.string(), Order: z.string(),
}) })
.strict(); .strict();
const ReplicatedSchema = z const ReplicatedSchema = z
.object({ .object({
Replicas: z.number().optional(), Replicas: z.number().optional(),
}) })
.strict(); .strict();
const ReplicatedJobSchema = z const ReplicatedJobSchema = z
.object({ .object({
MaxConcurrent: z.number().optional(), MaxConcurrent: z.number().optional(),
TotalCompletions: z.number().optional(), TotalCompletions: z.number().optional(),
}) })
.strict(); .strict();
const ServiceModeSwarmSchema = z const ServiceModeSwarmSchema = z
.object({ .object({
Replicated: ReplicatedSchema.optional(), Replicated: ReplicatedSchema.optional(),
Global: z.object({}).optional(), Global: z.object({}).optional(),
ReplicatedJob: ReplicatedJobSchema.optional(), ReplicatedJob: ReplicatedJobSchema.optional(),
GlobalJob: z.object({}).optional(), GlobalJob: z.object({}).optional(),
}) })
.strict(); .strict();
const NetworkSwarmSchema = z.array( const NetworkSwarmSchema = z.array(
z z
.object({ .object({
Target: z.string().optional(), Target: z.string().optional(),
Aliases: z.array(z.string()).optional(), Aliases: z.array(z.string()).optional(),
DriverOpts: z.object({}).optional(), DriverOpts: z.object({}).optional(),
}) })
.strict() .strict(),
); );
const LabelsSwarmSchema = z.record(z.string()); const LabelsSwarmSchema = z.record(z.string());
const createSchema = createInsertSchema(applications, { const createSchema = createInsertSchema(applications, {
appName: z.string(), appName: z.string(),
createdAt: z.string(), createdAt: z.string(),
applicationId: z.string(), applicationId: z.string(),
autoDeploy: z.boolean(), autoDeploy: z.boolean(),
env: z.string().optional(), env: z.string().optional(),
buildArgs: z.string().optional(), buildArgs: z.string().optional(),
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(), description: z.string().optional(),
memoryReservation: z.number().optional(), memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(), memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(), cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(), cpuLimit: z.number().optional(),
title: z.string().optional(), title: z.string().optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
subtitle: z.string().optional(), subtitle: z.string().optional(),
dockerImage: z.string().optional(), dockerImage: z.string().optional(),
username: z.string().optional(), username: z.string().optional(),
isPreviewDeploymentsActive: z.boolean().optional(), isPreviewDeploymentsActive: z.boolean().optional(),
password: z.string().optional(), password: z.string().optional(),
registryUrl: z.string().optional(), registryUrl: z.string().optional(),
customGitSSHKeyId: z.string().optional(), customGitSSHKeyId: z.string().optional(),
repository: z.string().optional(), repository: z.string().optional(),
dockerfile: z.string().optional(), dockerfile: z.string().optional(),
branch: z.string().optional(), branch: z.string().optional(),
customGitBranch: z.string().optional(), customGitBranch: z.string().optional(),
customGitBuildPath: z.string().optional(), customGitBuildPath: z.string().optional(),
customGitUrl: z.string().optional(), customGitUrl: z.string().optional(),
buildPath: z.string().optional(), buildPath: z.string().optional(),
projectId: z.string(), projectId: z.string(),
sourceType: z.enum(["github", "docker", "git"]).optional(), sourceType: z.enum(["github", "docker", "git"]).optional(),
applicationStatus: z.enum(["idle", "running", "done", "error"]), applicationStatus: z.enum(["idle", "running", "done", "error"]),
buildType: z.enum([ buildType: z.enum([
"dockerfile", "dockerfile",
"heroku_buildpacks", "heroku_buildpacks",
"paketo_buildpacks", "paketo_buildpacks",
"nixpacks", "nixpacks",
"static", "static",
]), ]),
herokuVersion: z.string().optional(), herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(), publishDirectory: z.string().optional(),
owner: z.string(), owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(), healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(), restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(), placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(), updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(), rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
previewPort: z.number().optional(), previewPort: z.number().optional(),
previewEnv: z.string().optional(), previewEnv: z.string().optional(),
previewBuildArgs: z.string().optional(), previewBuildArgs: z.string().optional(),
previewWildcard: z.string().optional(), previewWildcard: z.string().optional(),
previewLimit: z.number().optional(), previewLimit: z.number().optional(),
previewHttps: z.boolean().optional(), previewHttps: z.boolean().optional(),
previewPath: z.string().optional(), previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none"]).optional(), previewCertificateType: z.enum(["letsencrypt", "none"]).optional(),
}); });
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({
name: true, name: true,
appName: true, appName: true,
description: true, description: true,
projectId: true, projectId: true,
serverId: true, serverId: true,
}); });
export const apiFindOneApplication = createSchema export const apiFindOneApplication = createSchema
.pick({ .pick({
applicationId: true, applicationId: true,
}) })
.required(); .required();
export const apiReloadApplication = createSchema export const apiReloadApplication = createSchema
.pick({ .pick({
appName: true, appName: true,
applicationId: true, applicationId: true,
}) })
.required(); .required();
export const apiSaveBuildType = createSchema export const apiSaveBuildType = createSchema
.pick({ .pick({
applicationId: true, applicationId: true,
buildType: true, buildType: true,
dockerfile: true, dockerfile: true,
dockerContextPath: true, dockerContextPath: true,
dockerBuildStage: true, dockerBuildStage: true,
herokuVersion: true, herokuVersion: true,
}) })
.required() .required()
.merge(createSchema.pick({ publishDirectory: true })); .merge(createSchema.pick({ publishDirectory: true }));
export const apiSaveGithubProvider = createSchema export const apiSaveGithubProvider = createSchema
.pick({ .pick({
applicationId: true, applicationId: true,
repository: true, repository: true,
branch: true, branch: true,
owner: true, owner: true,
buildPath: true, buildPath: true,
githubId: true, githubId: true,
}) })
.required(); .required();
export const apiSaveGitlabProvider = createSchema export const apiSaveGitlabProvider = createSchema
.pick({ .pick({
applicationId: true, applicationId: true,
gitlabBranch: true, gitlabBranch: true,
gitlabBuildPath: true, gitlabBuildPath: true,
gitlabOwner: true, gitlabOwner: true,
gitlabRepository: true, gitlabRepository: true,
gitlabId: true, gitlabId: true,
gitlabProjectId: true, gitlabProjectId: true,
gitlabPathNamespace: true, gitlabPathNamespace: true,
}) })
.required(); .required();
export const apiSaveBitbucketProvider = createSchema export const apiSaveBitbucketProvider = createSchema
.pick({ .pick({
bitbucketBranch: true, bitbucketBranch: true,
bitbucketBuildPath: true, bitbucketBuildPath: true,
bitbucketOwner: true, bitbucketOwner: true,
bitbucketRepository: true, bitbucketRepository: true,
bitbucketId: true, bitbucketId: true,
applicationId: true, applicationId: true,
}) })
.required(); .required();
export const apiSaveDockerProvider = createSchema export const apiSaveDockerProvider = createSchema
.pick({ .pick({
dockerImage: true, dockerImage: true,
applicationId: true, applicationId: true,
username: true, username: true,
password: true, password: true,
registryUrl: true, registryUrl: true,
}) })
.required(); .required();
export const apiSaveGitProvider = createSchema export const apiSaveGitProvider = createSchema
.pick({ .pick({
customGitBranch: true, customGitBranch: true,
applicationId: true, applicationId: true,
customGitBuildPath: true, customGitBuildPath: true,
customGitUrl: true, customGitUrl: true,
}) })
.required() .required()
.merge( .merge(
createSchema.pick({ createSchema.pick({
customGitSSHKeyId: true, customGitSSHKeyId: true,
}) }),
); );
export const apiSaveEnvironmentVariables = createSchema export const apiSaveEnvironmentVariables = createSchema
.pick({ .pick({
applicationId: true, applicationId: true,
env: true, env: true,
buildArgs: true, buildArgs: true,
}) })
.required(); .required();
export const apiFindMonitoringStats = createSchema export const apiFindMonitoringStats = createSchema
.pick({ .pick({
appName: true, appName: true,
}) })
.required(); .required();
export const apiUpdateApplication = createSchema export const apiUpdateApplication = createSchema
.partial() .partial()
.extend({ .extend({
applicationId: z.string().min(1), applicationId: z.string().min(1),
}) })
.omit({ serverId: true }); .omit({ serverId: true });

View File

@@ -11,8 +11,8 @@ import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { compose } from "./compose"; import { compose } from "./compose";
import { server } from "./server";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { server } from "./server";
export const deploymentStatus = pgEnum("deploymentStatus", [ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",

View File

@@ -14,8 +14,8 @@ import { z } from "zod";
import { domain } from "../validations/domain"; import { domain } from "../validations/domain";
import { applications } from "./application"; import { applications } from "./application";
import { compose } from "./compose"; import { compose } from "./compose";
import { certificateType } from "./shared";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { certificateType } from "./shared";
export const domainType = pgEnum("domainType", [ export const domainType = pgEnum("domainType", [
"compose", "compose",

View File

@@ -29,4 +29,4 @@ export * from "./github";
export * from "./gitlab"; export * from "./gitlab";
export * from "./server"; export * from "./server";
export * from "./utils"; export * from "./utils";
export * from "./preview-deployments"; export * from "./preview-deployments";

View File

@@ -1,13 +1,13 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { pgTable, text } from "drizzle-orm/pg-core"; import { pgTable, text } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { applications } from "./application";
import { domains } from "./domain";
import { deployments } from "./deployment";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { generateAppName } from "./utils"; import { applications } from "./application";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { applicationStatus } from "./shared"; import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
export const previewDeployments = pgTable("preview_deployments", { export const previewDeployments = pgTable("preview_deployments", {
previewDeploymentId: text("previewDeploymentId") previewDeploymentId: text("previewDeploymentId")

View File

@@ -7,12 +7,12 @@ import {
cleanUpSystemPrune, cleanUpSystemPrune,
cleanUpUnusedImages, cleanUpUnusedImages,
} from "../docker/utils"; } from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { runMariadbBackup } from "./mariadb"; import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo"; import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql"; import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres"; import { runPostgresBackup } from "./postgres";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
export const initCronJobs = async () => { export const initCronJobs = async () => {
console.log("Setting up cron jobs...."); console.log("Setting up cron jobs....");

View File

@@ -2,6 +2,7 @@ import { createWriteStream } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode"; import type { CreateServiceOptions } from "dockerode";
import { nanoid } from "nanoid";
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
import { import {
calculateResources, calculateResources,
@@ -17,7 +18,6 @@ import { buildHeroku, getHerokuCommand } from "./heroku";
import { buildNixpacks, getNixpacksCommand } from "./nixpacks"; import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
import { buildPaketo, getPaketoCommand } from "./paketo"; import { buildPaketo, getPaketoCommand } from "./paketo";
import { buildStatic, getStaticCommand } from "./static"; import { buildStatic, getStaticCommand } from "./static";
import { nanoid } from "nanoid";
// NIXPACKS codeDirectory = where is the path of the code directory // NIXPACKS codeDirectory = where is the path of the code directory
// HEROKU codeDirectory = where is the path of the code directory // HEROKU codeDirectory = where is the path of the code directory

View File

@@ -15,528 +15,528 @@ import { spawnAsync } from "../process/spawnAsync";
import { getRemoteDocker } from "../servers/remote-docker"; import { getRemoteDocker } from "../servers/remote-docker";
interface RegistryAuth { interface RegistryAuth {
username: string; username: string;
password: string; password: string;
registryUrl: string; registryUrl: string;
} }
export const pullImage = async ( export const pullImage = async (
dockerImage: string, dockerImage: string,
onData?: (data: any) => void, onData?: (data: any) => void,
authConfig?: Partial<RegistryAuth> authConfig?: Partial<RegistryAuth>,
): Promise<void> => { ): Promise<void> => {
try { try {
if (!dockerImage) { if (!dockerImage) {
throw new Error("Docker image not found"); throw new Error("Docker image not found");
} }
if (authConfig?.username && authConfig?.password) { if (authConfig?.username && authConfig?.password) {
await spawnAsync( await spawnAsync(
"docker", "docker",
[ [
"login", "login",
authConfig.registryUrl || "", authConfig.registryUrl || "",
"-u", "-u",
authConfig.username, authConfig.username,
"-p", "-p",
authConfig.password, authConfig.password,
], ],
onData onData,
); );
} }
await spawnAsync("docker", ["pull", dockerImage], onData); await spawnAsync("docker", ["pull", dockerImage], onData);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const pullRemoteImage = async ( export const pullRemoteImage = async (
dockerImage: string, dockerImage: string,
serverId: string, serverId: string,
onData?: (data: any) => void, onData?: (data: any) => void,
authConfig?: Partial<RegistryAuth> authConfig?: Partial<RegistryAuth>,
): Promise<void> => { ): Promise<void> => {
try { try {
if (!dockerImage) { if (!dockerImage) {
throw new Error("Docker image not found"); throw new Error("Docker image not found");
} }
const remoteDocker = await getRemoteDocker(serverId); const remoteDocker = await getRemoteDocker(serverId);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
remoteDocker.pull( remoteDocker.pull(
dockerImage, dockerImage,
{ authconfig: authConfig }, { authconfig: authConfig },
(err, stream) => { (err, stream) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
} }
remoteDocker.modem.followProgress( remoteDocker.modem.followProgress(
stream as Readable, stream as Readable,
(err: Error | null, res) => { (err: Error | null, res) => {
if (!err) { if (!err) {
resolve(res); resolve(res);
} }
if (err) { if (err) {
reject(err); reject(err);
} }
}, },
(event) => { (event) => {
onData?.(event); onData?.(event);
} },
); );
} },
); );
}); });
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const containerExists = async (containerName: string) => { export const containerExists = async (containerName: string) => {
const container = docker.getContainer(containerName); const container = docker.getContainer(containerName);
try { try {
await container.inspect(); await container.inspect();
return true; return true;
} catch (error) { } catch (error) {
return false; return false;
} }
}; };
export const stopService = async (appName: string) => { export const stopService = async (appName: string) => {
try { try {
await execAsync(`docker service scale ${appName}=0 `); await execAsync(`docker service scale ${appName}=0 `);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return error; return error;
} }
}; };
export const stopServiceRemote = async (serverId: string, appName: string) => { export const stopServiceRemote = async (serverId: string, appName: string) => {
try { try {
await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); await execAsyncRemote(serverId, `docker service scale ${appName}=0 `);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return error; return error;
} }
}; };
export const getContainerByName = (name: string): Promise<ContainerInfo> => { export const getContainerByName = (name: string): Promise<ContainerInfo> => {
const opts = { const opts = {
limit: 1, limit: 1,
filters: { filters: {
name: [name], name: [name],
}, },
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
docker.listContainers(opts, (err, containers) => { docker.listContainers(opts, (err, containers) => {
if (err) { if (err) {
reject(err); reject(err);
} else if (containers?.length === 0) { } else if (containers?.length === 0) {
reject(new Error(`No container found with name: ${name}`)); reject(new Error(`No container found with name: ${name}`));
} else if (containers && containers?.length > 0 && containers[0]) { } else if (containers && containers?.length > 0 && containers[0]) {
resolve(containers[0]); resolve(containers[0]);
} }
}); });
}); });
}; };
export const cleanUpUnusedImages = async (serverId?: string) => { export const cleanUpUnusedImages = async (serverId?: string) => {
try { try {
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, "docker image prune --all --force"); await execAsyncRemote(serverId, "docker image prune --all --force");
} else { } else {
await execAsync("docker image prune --all --force"); await execAsync("docker image prune --all --force");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw error; throw error;
} }
}; };
export const cleanStoppedContainers = async (serverId?: string) => { export const cleanStoppedContainers = async (serverId?: string) => {
try { try {
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, "docker container prune --force"); await execAsyncRemote(serverId, "docker container prune --force");
} else { } else {
await execAsync("docker container prune --force"); await execAsync("docker container prune --force");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw error; throw error;
} }
}; };
export const cleanUpUnusedVolumes = async (serverId?: string) => { export const cleanUpUnusedVolumes = async (serverId?: string) => {
try { try {
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, "docker volume prune --all --force"); await execAsyncRemote(serverId, "docker volume prune --all --force");
} else { } else {
await execAsync("docker volume prune --all --force"); await execAsync("docker volume prune --all --force");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw error; throw error;
} }
}; };
export const cleanUpInactiveContainers = async () => { export const cleanUpInactiveContainers = async () => {
try { try {
const containers = await docker.listContainers({ all: true }); const containers = await docker.listContainers({ all: true });
const inactiveContainers = containers.filter( const inactiveContainers = containers.filter(
(container) => container.State !== "running" (container) => container.State !== "running",
); );
for (const container of inactiveContainers) { for (const container of inactiveContainers) {
await docker.getContainer(container.Id).remove({ force: true }); await docker.getContainer(container.Id).remove({ force: true });
console.log(`Cleaning up inactive container: ${container.Id}`); console.log(`Cleaning up inactive container: ${container.Id}`);
} }
} catch (error) { } catch (error) {
console.error("Error cleaning up inactive containers:", error); console.error("Error cleaning up inactive containers:", error);
throw error; throw error;
} }
}; };
export const cleanUpDockerBuilder = async (serverId?: string) => { export const cleanUpDockerBuilder = async (serverId?: string) => {
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, "docker builder prune --all --force"); await execAsyncRemote(serverId, "docker builder prune --all --force");
} else { } else {
await execAsync("docker builder prune --all --force"); await execAsync("docker builder prune --all --force");
} }
}; };
export const cleanUpSystemPrune = async (serverId?: string) => { export const cleanUpSystemPrune = async (serverId?: string) => {
if (serverId) { if (serverId) {
await execAsyncRemote( await execAsyncRemote(
serverId, serverId,
"docker system prune --all --force --volumes" "docker system prune --all --force --volumes",
); );
} else { } else {
await execAsync("docker system prune --all --force --volumes"); await execAsync("docker system prune --all --force --volumes");
} }
}; };
export const startService = async (appName: string) => { export const startService = async (appName: string) => {
try { try {
await execAsync(`docker service scale ${appName}=1 `); await execAsync(`docker service scale ${appName}=1 `);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw error; throw error;
} }
}; };
export const startServiceRemote = async (serverId: string, appName: string) => { export const startServiceRemote = async (serverId: string, appName: string) => {
try { try {
await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); await execAsyncRemote(serverId, `docker service scale ${appName}=1 `);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw error; throw error;
} }
}; };
export const removeService = async ( export const removeService = async (
appName: string, appName: string,
serverId?: string | null, serverId?: string | null,
deleteVolumes = false deleteVolumes = false,
) => { ) => {
try { try {
let command: string; let command: string;
if (deleteVolumes) { if (deleteVolumes) {
command = `docker service rm --force ${appName}`; command = `docker service rm --force ${appName}`;
} else { } else {
command = `docker service rm ${appName}`; command = `docker service rm ${appName}`;
} }
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} else { } else {
await execAsync(command); await execAsync(command);
} }
} catch (error) { } catch (error) {
return error; return error;
} }
}; };
export const prepareEnvironmentVariables = ( export const prepareEnvironmentVariables = (
serviceEnv: string | null, serviceEnv: string | null,
projectEnv?: string | null projectEnv?: string | null,
) => { ) => {
const projectVars = parse(projectEnv ?? ""); const projectVars = parse(projectEnv ?? "");
const serviceVars = parse(serviceEnv ?? ""); const serviceVars = parse(serviceEnv ?? "");
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => { const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
let resolvedValue = value; let resolvedValue = value;
if (projectVars) { if (projectVars) {
resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => { resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
if (projectVars[ref] !== undefined) { if (projectVars[ref] !== undefined) {
return projectVars[ref]; return projectVars[ref];
} }
throw new Error(`Invalid project environment variable: project.${ref}`); throw new Error(`Invalid project environment variable: project.${ref}`);
}); });
} }
return `${key}=${resolvedValue}`; return `${key}=${resolvedValue}`;
}); });
return resolvedVars; return resolvedVars;
}; };
export const prepareBuildArgs = (input: string | null) => { export const prepareBuildArgs = (input: string | null) => {
const pairs = (input ?? "").split("\n"); const pairs = (input ?? "").split("\n");
const jsonObject: Record<string, string> = {}; const jsonObject: Record<string, string> = {};
for (const pair of pairs) { for (const pair of pairs) {
const [key, value] = pair.split("="); const [key, value] = pair.split("=");
if (key && value) { if (key && value) {
jsonObject[key] = value; jsonObject[key] = value;
} }
} }
return jsonObject; return jsonObject;
}; };
export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => { export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => {
if (!mounts || mounts.length === 0) { if (!mounts || mounts.length === 0) {
return []; return [];
} }
return mounts return mounts
.filter((mount) => mount.type === "volume") .filter((mount) => mount.type === "volume")
.map((mount) => ({ .map((mount) => ({
Type: "volume" as const, Type: "volume" as const,
Source: mount.volumeName || "", Source: mount.volumeName || "",
Target: mount.mountPath, Target: mount.mountPath,
})); }));
}; };
type Resources = { type Resources = {
memoryLimit: number | null; memoryLimit: number | null;
memoryReservation: number | null; memoryReservation: number | null;
cpuLimit: number | null; cpuLimit: number | null;
cpuReservation: number | null; cpuReservation: number | null;
}; };
export const calculateResources = ({ export const calculateResources = ({
memoryLimit, memoryLimit,
memoryReservation, memoryReservation,
cpuLimit, cpuLimit,
cpuReservation, cpuReservation,
}: Resources): ResourceRequirements => { }: Resources): ResourceRequirements => {
return { return {
Limits: { Limits: {
MemoryBytes: memoryLimit ?? undefined, MemoryBytes: memoryLimit ?? undefined,
NanoCPUs: cpuLimit ?? undefined, NanoCPUs: cpuLimit ?? undefined,
}, },
Reservations: { Reservations: {
MemoryBytes: memoryReservation ?? undefined, MemoryBytes: memoryReservation ?? undefined,
NanoCPUs: cpuReservation ?? undefined, NanoCPUs: cpuReservation ?? undefined,
}, },
}; };
}; };
export const generateConfigContainer = (application: ApplicationNested) => { export const generateConfigContainer = (application: ApplicationNested) => {
const { const {
healthCheckSwarm, healthCheckSwarm,
restartPolicySwarm, restartPolicySwarm,
placementSwarm, placementSwarm,
updateConfigSwarm, updateConfigSwarm,
rollbackConfigSwarm, rollbackConfigSwarm,
modeSwarm, modeSwarm,
labelsSwarm, labelsSwarm,
replicas, replicas,
mounts, mounts,
networkSwarm, networkSwarm,
} = application; } = application;
const haveMounts = mounts.length > 0; const haveMounts = mounts.length > 0;
return { return {
...(healthCheckSwarm && { ...(healthCheckSwarm && {
HealthCheck: healthCheckSwarm, HealthCheck: healthCheckSwarm,
}), }),
...(restartPolicySwarm ...(restartPolicySwarm
? { ? {
RestartPolicy: restartPolicySwarm, RestartPolicy: restartPolicySwarm,
} }
: {}), : {}),
...(placementSwarm ...(placementSwarm
? { ? {
Placement: placementSwarm, Placement: placementSwarm,
} }
: { : {
// if app have mounts keep manager as constraint // if app have mounts keep manager as constraint
Placement: { Placement: {
Constraints: haveMounts ? ["node.role==manager"] : [], Constraints: haveMounts ? ["node.role==manager"] : [],
}, },
}), }),
...(labelsSwarm && { ...(labelsSwarm && {
Labels: labelsSwarm, Labels: labelsSwarm,
}), }),
...(modeSwarm ...(modeSwarm
? { ? {
Mode: modeSwarm, Mode: modeSwarm,
} }
: { : {
// use replicas value if no modeSwarm provided // use replicas value if no modeSwarm provided
Mode: { Mode: {
Replicated: { Replicated: {
Replicas: replicas, Replicas: replicas,
}, },
}, },
}), }),
...(rollbackConfigSwarm && { ...(rollbackConfigSwarm && {
RollbackConfig: rollbackConfigSwarm, RollbackConfig: rollbackConfigSwarm,
}), }),
...(updateConfigSwarm ...(updateConfigSwarm
? { UpdateConfig: updateConfigSwarm } ? { UpdateConfig: updateConfigSwarm }
: { : {
// default config if no updateConfigSwarm provided // default config if no updateConfigSwarm provided
UpdateConfig: { UpdateConfig: {
Parallelism: 1, Parallelism: 1,
Order: "start-first", Order: "start-first",
}, },
}), }),
...(networkSwarm ...(networkSwarm
? { ? {
Networks: networkSwarm, Networks: networkSwarm,
} }
: { : {
Networks: [{ Target: "dokploy-network" }], Networks: [{ Target: "dokploy-network" }],
}), }),
}; };
}; };
export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => { export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => {
if (!mounts || mounts.length === 0) { if (!mounts || mounts.length === 0) {
return []; return [];
} }
return mounts return mounts
.filter((mount) => mount.type === "bind") .filter((mount) => mount.type === "bind")
.map((mount) => ({ .map((mount) => ({
Type: "bind" as const, Type: "bind" as const,
Source: mount.hostPath || "", Source: mount.hostPath || "",
Target: mount.mountPath, Target: mount.mountPath,
})); }));
}; };
export const generateFileMounts = ( export const generateFileMounts = (
appName: string, appName: string,
service: service:
| ApplicationNested | ApplicationNested
| MongoNested | MongoNested
| MariadbNested | MariadbNested
| MysqlNested | MysqlNested
| PostgresNested | PostgresNested
| RedisNested | RedisNested,
) => { ) => {
const { mounts } = service; const { mounts } = service;
const { APPLICATIONS_PATH } = paths(!!service.serverId); const { APPLICATIONS_PATH } = paths(!!service.serverId);
if (!mounts || mounts.length === 0) { if (!mounts || mounts.length === 0) {
return []; return [];
} }
return mounts return mounts
.filter((mount) => mount.type === "file") .filter((mount) => mount.type === "file")
.map((mount) => { .map((mount) => {
const fileName = mount.filePath; const fileName = mount.filePath;
const absoluteBasePath = path.resolve(APPLICATIONS_PATH); const absoluteBasePath = path.resolve(APPLICATIONS_PATH);
const directory = path.join(absoluteBasePath, appName, "files"); const directory = path.join(absoluteBasePath, appName, "files");
const sourcePath = path.join(directory, fileName || ""); const sourcePath = path.join(directory, fileName || "");
return { return {
Type: "bind" as const, Type: "bind" as const,
Source: sourcePath, Source: sourcePath,
Target: mount.mountPath, Target: mount.mountPath,
}; };
}); });
}; };
export const createFile = async ( export const createFile = async (
outputPath: string, outputPath: string,
filePath: string, filePath: string,
content: string content: string,
) => { ) => {
try { try {
const fullPath = path.join(outputPath, filePath); const fullPath = path.join(outputPath, filePath);
if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) {
fs.mkdirSync(fullPath, { recursive: true }); fs.mkdirSync(fullPath, { recursive: true });
return; return;
} }
const directory = path.dirname(fullPath); const directory = path.dirname(fullPath);
fs.mkdirSync(directory, { recursive: true }); fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(fullPath, content || ""); fs.writeFileSync(fullPath, content || "");
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const encodeBase64 = (content: string) => export const encodeBase64 = (content: string) =>
Buffer.from(content, "utf-8").toString("base64"); Buffer.from(content, "utf-8").toString("base64");
export const getCreateFileCommand = ( export const getCreateFileCommand = (
outputPath: string, outputPath: string,
filePath: string, filePath: string,
content: string content: string,
) => { ) => {
const fullPath = path.join(outputPath, filePath); const fullPath = path.join(outputPath, filePath);
if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) {
return `mkdir -p ${fullPath};`; return `mkdir -p ${fullPath};`;
} }
const directory = path.dirname(fullPath); const directory = path.dirname(fullPath);
const encodedContent = encodeBase64(content); const encodedContent = encodeBase64(content);
return ` return `
mkdir -p ${directory}; mkdir -p ${directory};
echo "${encodedContent}" | base64 -d > "${fullPath}"; echo "${encodedContent}" | base64 -d > "${fullPath}";
`; `;
}; };
export const getServiceContainer = async (appName: string) => { export const getServiceContainer = async (appName: string) => {
try { try {
const filter = { const filter = {
status: ["running"], status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`], label: [`com.docker.swarm.service.name=${appName}`],
}; };
const containers = await docker.listContainers({ const containers = await docker.listContainers({
filters: JSON.stringify(filter), filters: JSON.stringify(filter),
}); });
if (containers.length === 0 || !containers[0]) { if (containers.length === 0 || !containers[0]) {
throw new Error(`No container found with name: ${appName}`); throw new Error(`No container found with name: ${appName}`);
} }
const container = containers[0]; const container = containers[0];
return container; return container;
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const getRemoteServiceContainer = async ( export const getRemoteServiceContainer = async (
serverId: string, serverId: string,
appName: string appName: string,
) => { ) => {
try { try {
const filter = { const filter = {
status: ["running"], status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`], label: [`com.docker.swarm.service.name=${appName}`],
}; };
const remoteDocker = await getRemoteDocker(serverId); const remoteDocker = await getRemoteDocker(serverId);
const containers = await remoteDocker.listContainers({ const containers = await remoteDocker.listContainers({
filters: JSON.stringify(filter), filters: JSON.stringify(filter),
}); });
if (containers.length === 0 || !containers[0]) { if (containers.length === 0 || !containers[0]) {
throw new Error(`No container found with name: ${appName}`); throw new Error(`No container found with name: ${appName}`);
} }
const container = containers[0]; const container = containers[0];
return container; return container;
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };