Merge branch 'canary' into feat/add-gitea-repo

This commit is contained in:
Jason Parks
2025-03-17 15:25:02 -06:00
committed by GitHub
28 changed files with 2109 additions and 1230 deletions

View File

@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
cleanCache: false,
watchPaths: [],
applicationStatus: "done",
appName: "",

View File

@@ -7,6 +7,7 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
cleanCache: false,
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@@ -16,8 +16,8 @@ import {
Ban,
CheckCircle2,
Hammer,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useRouter } from "next/router";
@@ -55,7 +55,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
<DialogAction
title="Deploy Application"
description="Are you sure you want to deploy this application?"
@@ -79,12 +79,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -114,9 +116,24 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Reload the application without rebuilding it</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
<DialogAction
@@ -139,13 +156,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="secondary"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Rebuild
<Hammer className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Hammer className="size-4 mr-1" />
Rebuild
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -180,13 +198,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -219,13 +238,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -241,15 +261,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
aria-label="Toggle autodeploy"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
@@ -264,14 +287,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error updating Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/>
</div>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Clean Cache</span>
<Switch
aria-label="Toggle italic"
aria-label="Toggle clean cache"
checked={data?.cleanCache || false}
onCheckedChange={async (enabled) => {
await update({
@@ -286,7 +309,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error updating Clean Cache");
});
}}
className="flex flex-row gap-2 items-center"
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/>
</div>
</CardContent>

View File

@@ -8,8 +8,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal, RefreshCcw, Rocket } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -34,7 +33,7 @@ export const ComposeActions = ({ composeId }: Props) => {
api.compose.stop.useMutation();
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">
<TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
<DialogAction
title="Deploy Compose"
description="Are you sure you want to deploy this compose?"
@@ -58,12 +57,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button
variant="default"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -74,36 +75,37 @@ export const ComposeActions = ({ composeId }: Props) => {
</Button>
</DialogAction>
<DialogAction
title="Rebuild Compose"
description="Are you sure you want to rebuild this compose?"
title="Reload Compose"
description="Are you sure you want to reload this compose?"
type="default"
onClick={async () => {
await redeploy({
composeId: composeId,
})
.then(() => {
toast.success("Compose rebuilt successfully");
toast.success("Compose reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding compose");
toast.error("Error reloading compose");
});
}}
>
<Button
variant="secondary"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Rebuild
<Hammer className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Only rebuilds the compose without downloading new code</p>
<p>Reload the compose without rebuilding it</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
@@ -131,13 +133,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -169,13 +172,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -191,15 +195,18 @@ export const ComposeActions = ({ composeId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
aria-label="Toggle autodeploy"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
@@ -214,7 +221,7 @@ export const ComposeActions = ({ composeId }: Props) => {
toast.error("Error updating Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/>
</div>
</div>

View File

@@ -0,0 +1,367 @@
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources";
import { debounce } from "lodash";
import { Input } from "@/components/ui/input";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
interface Props {
databaseId: string;
databaseType: Exclude<ServiceType, "application" | "redis">;
}
const RestoreBackupSchema = z.object({
destinationId: z
.string({
required_error: "Please select a destination",
})
.min(1, {
message: "Destination is required",
}),
backupFile: z
.string({
required_error: "Please select a backup file",
})
.min(1, {
message: "Backup file is required",
}),
databaseName: z
.string({
required_error: "Please enter a database name",
})
.min(1, {
message: "Database name is required",
}),
});
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState("");
const { data: destinations = [] } = api.destination.all.useQuery();
const form = useForm<RestoreBackup>({
defaultValues: {
destinationId: "",
backupFile: "",
databaseName: "",
},
resolver: zodResolver(RestoreBackupSchema),
});
const destionationId = form.watch("destinationId");
const debouncedSetSearch = debounce((value: string) => {
setSearch(value);
}, 300);
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{
destinationId: destionationId,
search,
},
{
enabled: isOpen && !!destionationId,
},
);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
// const { mutateAsync: restore, isLoading: isRestoring } =
// api.backup.restoreBackup.useMutation();
api.backup.restoreBackupWithLogs.useSubscription(
{
databaseId,
databaseType,
databaseName: form.watch("databaseName"),
backupFile: form.watch("backupFile"),
destinationId: form.watch("destinationId"),
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
if (log === "Restore completed successfully!") {
setIsDeploying(false);
}
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Restore logs error:", error);
setIsDeploying(false);
},
},
);
const onSubmit = async (_data: RestoreBackup) => {
setIsDeploying(true);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">
<RotateCcw className="mr-2 size-4" />
Restore Backup
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center">
<RotateCcw className="mr-2 size-4" />
Restore Backup
</DialogTitle>
<DialogDescription>
Select a destination and search for backup files
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
id="hook-form-restore-backup"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="destinationId"
render={({ field }) => (
<FormItem className="">
<FormLabel>Destination</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{field.value
? destinations.find(
(d) => d.destinationId === field.value,
)?.name
: "Select Destination"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search destinations..."
className="h-9"
/>
<CommandEmpty>No destinations found.</CommandEmpty>
<ScrollArea className="h-64">
<CommandGroup>
{destinations.map((destination) => (
<CommandItem
value={destination.destinationId}
key={destination.destinationId}
onSelect={() => {
form.setValue(
"destinationId",
destination.destinationId,
);
}}
>
{destination.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
destination.destinationId === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="backupFile"
render={({ field }) => (
<FormItem className="">
<FormLabel className="flex items-center justify-between">
Search Backup Files
{field.value && (
<Badge variant="outline">
{field.value}
<Copy
className="ml-2 size-4 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
copy(field.value);
toast.success("Backup file copied to clipboard");
}}
/>
</Badge>
)}
</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{field.value || "Search and select a backup file"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search backup files..."
onValueChange={debouncedSetSearch}
className="h-9"
/>
{isLoading ? (
<div className="py-6 text-center text-sm">
Loading backup files...
</div>
) : files.length === 0 && search ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No backup files found for "{search}"
</div>
) : files.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No backup files available
</div>
) : (
<ScrollArea className="h-64">
<CommandGroup>
{files.map((file) => (
<CommandItem
value={file}
key={file}
onSelect={() => {
form.setValue("backupFile", file);
}}
>
{file}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
)}
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="databaseName"
render={({ field }) => (
<FormItem className="">
<FormLabel>Database Name</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter database name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isDeploying}
form="hook-form-restore-backup"
type="submit"
disabled={!form.watch("backupFile")}
>
Restore
</Button>
</DialogFooter>
</form>
</Form>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
// refetch();
}}
filteredLogs={filteredLogs}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -21,6 +21,8 @@ import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources";
import { AddBackup } from "./add-backup";
import { UpdateBackup } from "./update-backup";
import { RestoreBackup } from "./restore-backup";
import { useState } from "react";
interface Props {
id: string;
@@ -71,7 +73,10 @@ export const ShowBackups = ({ id, type }: Props) => {
</div>
{postgres && postgres?.backups?.length > 0 && (
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
<RestoreBackup databaseId={id} databaseType={type} />
</div>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
@@ -98,11 +103,14 @@ export const ShowBackups = ({ id, type }: Props) => {
<span className="text-base text-muted-foreground">
No backups configured
</span>
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
<RestoreBackup databaseId={id} databaseType={type} />
</div>
</div>
) : (
<div className="flex flex-col pt-2">
@@ -183,6 +191,7 @@ export const ShowBackups = ({ id, type }: Props) => {
<TooltipContent>Run Manual Backup</TooltipContent>
</Tooltip>
</TooltipProvider>
<UpdateBackup
backupId={backup.backupId}
refetch={refetch}

View File

@@ -119,7 +119,6 @@ export const DockerLogsId: React.FC<Props> = ({
const wsUrl = `${protocol}//${
window.location.host
}/docker-container-logs?${params.toString()}`;
console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl);
const resetNoDataTimeout = () => {
@@ -136,7 +135,6 @@ export const DockerLogsId: React.FC<Props> = ({
ws.close();
return;
}
console.log("WebSocket connected");
resetNoDataTimeout();
};

View File

@@ -27,146 +27,145 @@ import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
mariadbId: string;
mariadbId: string;
}
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mariadbId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mariadbId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
form,
data?.databaseName,
data?.databaseUser,
getIp,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="3306"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
form,
data?.databaseName,
data?.databaseUser,
getIp,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="3306"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
@@ -92,12 +93,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -107,6 +110,8 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<DialogAction
title="Reload Mariadb"
description="Are you sure you want to reload this mariadb?"
@@ -128,13 +133,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -144,7 +150,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
</TooltipProvider>
{data?.applicationStatus === "idle" ? (
<TooltipProvider delayDuration={0}>
<DialogAction
title="Start Mariadb"
description="Are you sure you want to start this mariadb?"
@@ -165,13 +173,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -184,7 +193,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
) : (
</TooltipProvider>
) : (
<TooltipProvider delayDuration={0}>
<DialogAction
title="Stop Mariadb"
description="Are you sure you want to stop this mariadb?"
@@ -204,13 +215,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -220,15 +232,29 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
</TooltipProvider>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MariaDB container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -27,145 +27,144 @@ import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
mongoId: string;
mongoId: string;
}
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mongoId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mongoId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
};
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
};
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
form,
data?.databaseUser,
getIp,
]);
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
form,
data?.databaseUser,
getIp,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="27017"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="27017"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -3,246 +3,256 @@ import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props {
mongoId: string;
mongoId: string;
}
export const ShowGeneralMongo = ({ mongoId }: Props) => {
const { data, refetch } = api.mongo.one.useQuery(
{
mongoId,
},
{ enabled: !!mongoId },
);
const { data, refetch } = api.mongo.one.useQuery(
{
mongoId,
},
{ enabled: !!mongoId }
);
const { mutateAsync: reload, isLoading: isReloading } =
api.mongo.reload.useMutation();
const { mutateAsync: reload, isLoading: isReloading } =
api.mongo.reload.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
api.mongo.start.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
api.mongo.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
api.mongo.stop.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
api.mongo.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
api.mongo.deployWithLogs.useSubscription(
{
mongoId: mongoId,
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
api.mongo.deployWithLogs.useSubscription(
{
mongoId: mongoId,
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
if (log === "Deployment completed successfully!") {
setIsDeploying(false);
}
if (log === "Deployment completed successfully!") {
setIsDeploying(false);
}
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Deployment logs error:", error);
setIsDeploying(false);
},
},
);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mongo"
description="Are you sure you want to deploy this mongo?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the MongoDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
<DialogAction
title="Reload Mongo"
description="Are you sure you want to reload this mongo?"
type="default"
onClick={async () => {
await reload({
mongoId: mongoId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mongo reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mongo");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the MongoDB service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mongo"
description="Are you sure you want to start this mongo?"
type="default"
onClick={async () => {
await start({
mongoId: mongoId,
})
.then(() => {
toast.success("Mongo started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mongo");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the MongoDB database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Mongo"
description="Are you sure you want to stop this mongo?"
onClick={async () => {
await stop({
mongoId: mongoId,
})
.then(() => {
toast.success("Mongo stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mongo");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running MongoDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
</Button>
</DockerTerminalModal>
</CardContent>
</Card>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
</>
);
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Deployment logs error:", error);
setIsDeploying(false);
},
}
);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mongo"
description="Are you sure you want to deploy this mongo?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Rocket className="size-4 mr-1" />
Deploy
</Button>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the MongoDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</DialogAction>
<DialogAction
title="Reload Mongo"
description="Are you sure you want to reload this mongo?"
type="default"
onClick={async () => {
await reload({
mongoId: mongoId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mongo reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mongo");
});
}}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<RefreshCcw className="size-4 mr-1" />
Reload
</Button>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the MongoDB service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mongo"
description="Are you sure you want to start this mongo?"
type="default"
onClick={async () => {
await start({
mongoId: mongoId,
})
.then(() => {
toast.success("Mongo started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mongo");
});
}}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<CheckCircle2 className="size-4 mr-1" />
Start
</Button>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the MongoDB database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</DialogAction>
) : (
<DialogAction
title="Stop Mongo"
description="Are you sure you want to stop this mongo?"
onClick={async () => {
await stop({
mongoId: mongoId,
})
.then(() => {
toast.success("Mongo stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mongo");
});
}}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Ban className="size-4 mr-1" />
Stop
</Button>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running MongoDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</DialogAction>
)}
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4" />
Open Terminal
</Button>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MongoDB container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</DockerTerminalModal>
</CardContent>
</Card>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
</>
);
};

View File

@@ -27,145 +27,144 @@ import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
mysqlId: string;
mysqlId: string;
}
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mysqlId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mysqlId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
data?.databaseName,
data?.databaseUser,
form,
getIp,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="3306"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput disabled value={connectionUrl} />
</div>
</div>
)}
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
data?.databaseName,
data?.databaseUser,
form,
getIp,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="3306"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput disabled value={connectionUrl} />
</div>
</div>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
@@ -77,7 +78,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mysql"
title="Deploy MySQL"
description="Are you sure you want to deploy this mysql?"
type="default"
onClick={async () => {
@@ -89,12 +90,14 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -105,7 +108,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</Button>
</DialogAction>
<DialogAction
title="Reload Mysql"
title="Reload MySQL"
description="Are you sure you want to reload this mysql?"
type="default"
onClick={async () => {
@@ -114,24 +117,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
appName: data?.appName || "",
})
.then(() => {
toast.success("Mysql reloaded successfully");
toast.success("MySQL reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mysql");
toast.error("Error reloading MySQL");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -143,7 +147,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mysql"
title="Start MySQL"
description="Are you sure you want to start this mysql?"
type="default"
onClick={async () => {
@@ -151,24 +155,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
mysqlId: mysqlId,
})
.then(() => {
toast.success("Mysql started successfully");
toast.success("MySQL started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mysql");
toast.error("Error starting MySQL");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -183,31 +188,32 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</DialogAction>
) : (
<DialogAction
title="Stop Mysql"
title="Stop MySQL"
description="Are you sure you want to stop this mysql?"
onClick={async () => {
await stop({
mysqlId: mysqlId,
})
.then(() => {
toast.success("Mysql stopped successfully");
toast.success("MySQL stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mysql");
toast.error("Error stopping MySQL");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -223,9 +229,23 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MySQL container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -27,147 +27,146 @@ import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
postgresId: string;
postgresId: string;
}
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
const { mutateAsync, isLoading } =
api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState("");
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
const { mutateAsync, isLoading } =
api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState("");
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
postgresId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
postgresId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
form,
data?.databaseName,
getIp,
]);
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
form,
data?.databaseName,
getIp,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="5432"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="5432"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
@@ -78,9 +79,9 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<TooltipProvider disableHoverableContent={false}>
<DialogAction
title="Deploy Postgres"
title="Deploy PostgreSQL"
description="Are you sure you want to deploy this postgres?"
type="default"
onClick={async () => {
@@ -92,12 +93,14 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -108,7 +111,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</Button>
</DialogAction>
<DialogAction
title="Reload Postgres"
title="Reload PostgreSQL"
description="Are you sure you want to reload this postgres?"
type="default"
onClick={async () => {
@@ -117,24 +120,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName: data?.appName || "",
})
.then(() => {
toast.success("Postgres reloaded successfully");
toast.success("PostgreSQL reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Postgres");
toast.error("Error reloading PostgreSQL");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -146,7 +150,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Postgres"
title="Start PostgreSQL"
description="Are you sure you want to start this postgres?"
type="default"
onClick={async () => {
@@ -154,24 +158,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres started successfully");
toast.success("PostgreSQL started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Postgres");
toast.error("Error starting PostgreSQL");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -186,31 +191,32 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</DialogAction>
) : (
<DialogAction
title="Stop Postgres"
title="Stop PostgreSQL"
description="Are you sure you want to stop this postgres?"
onClick={async () => {
await stop({
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres stopped successfully");
toast.success("PostgreSQL stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Postgres");
toast.error("Error stopping PostgreSQL");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -226,9 +232,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the PostgreSQL container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -5,58 +5,58 @@ import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
interface Props {
postgresId: string;
postgresId: string;
}
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
const { data } = api.postgres.one.useQuery({ postgresId });
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Internal Credentials</CardTitle>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2">
<Label>User</Label>
<Input disabled value={data?.databaseUser} />
</div>
<div className="flex flex-col gap-2">
<Label>Database Name</Label>
<Input disabled value={data?.databaseName} />
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<ToggleVisibilityInput
value={data?.databasePassword}
disabled
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Internal Port (Container)</Label>
<Input disabled value="5432" />
</div>
const { data } = api.postgres.one.useQuery({ postgresId });
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Internal Credentials</CardTitle>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2">
<Label>User</Label>
<Input disabled value={data?.databaseUser} />
</div>
<div className="flex flex-col gap-2">
<Label>Database Name</Label>
<Input disabled value={data?.databaseName} />
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<ToggleVisibilityInput
value={data?.databasePassword}
disabled
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Internal Port (Container)</Label>
<Input disabled value="5432" />
</div>
<div className="flex flex-col gap-2">
<Label>Internal Host</Label>
<Input disabled value={data?.appName} />
</div>
<div className="flex flex-col gap-2">
<Label>Internal Host</Label>
<Input disabled value={data?.appName} />
</div>
<div className="flex flex-col gap-2">
<Label>Internal Connection URL </Label>
<ToggleVisibilityInput
disabled
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
/>
</div>
</div>
</CardContent>
</Card>
</div>
</>
);
<div className="flex flex-col gap-2">
<Label>Internal Connection URL </Label>
<ToggleVisibilityInput
disabled
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
/>
</div>
</div>
</CardContent>
</Card>
</div>
</>
);
};
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w

View File

@@ -21,145 +21,146 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { PenBox } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const updatePostgresSchema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
});
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
interface Props {
postgresId: string;
postgresId: string;
}
export const UpdatePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.postgres.update.useMutation();
const { data } = api.postgres.one.useQuery(
{
postgresId,
},
{
enabled: !!postgresId,
},
);
const form = useForm<UpdatePostgres>({
defaultValues: {
description: data?.description ?? "",
name: data?.name ?? "",
},
resolver: zodResolver(updatePostgresSchema),
});
useEffect(() => {
if (data) {
form.reset({
description: data.description ?? "",
name: data.name,
});
}
}, [data, form, form.reset]);
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.postgres.update.useMutation();
const { data } = api.postgres.one.useQuery(
{
postgresId,
},
{
enabled: !!postgresId,
}
);
const form = useForm<UpdatePostgres>({
defaultValues: {
description: data?.description ?? "",
name: data?.name ?? "",
},
resolver: zodResolver(updatePostgresSchema),
});
useEffect(() => {
if (data) {
form.reset({
description: data.description ?? "",
name: data.name,
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: UpdatePostgres) => {
await mutateAsync({
name: formData.name,
postgresId: postgresId,
description: formData.description || "",
})
.then(() => {
toast.success("Postgres updated successfully");
utils.postgres.one.invalidate({
postgresId: postgresId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error updating Postgres");
})
.finally(() => {});
};
const onSubmit = async (formData: UpdatePostgres) => {
await mutateAsync({
name: formData.name,
postgresId: postgresId,
description: formData.description || "",
})
.then(() => {
toast.success("Postgres updated successfully");
utils.postgres.one.invalidate({
postgresId: postgresId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error updating Postgres");
})
.finally(() => {});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Postgres</DialogTitle>
<DialogDescription>Update the Postgres data</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Postgres</DialogTitle>
<DialogDescription>Update the Postgres data</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid gap-4">
<div className="grid items-center gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-update-postgres"
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Vandelay Industries" {...field} />
</FormControl>
<div className="grid gap-4">
<div className="grid items-center gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-update-postgres"
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Vandelay Industries" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description about your project..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description about your project..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-postgres"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</div>
</div>
</DialogContent>
</Dialog>
);
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-postgres"
type="submit"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -27,139 +27,138 @@ import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
redisId: string;
redisId: string;
}
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.redis.one.useQuery({ redisId });
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.redis.one.useQuery({ redisId });
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
redisId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
redisId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error saving the external port");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const _hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort;
useEffect(() => {
const buildConnectionUrl = () => {
const _hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort;
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
};
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
};
setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="6379"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="6379"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
@@ -91,12 +92,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -127,13 +130,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -164,13 +168,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -203,13 +208,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -225,9 +231,23 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the Redis container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
className={cn("text-left", className)}
ref={ref}
{...props}
value={props.value === undefined ? undefined : String(props.value)}
value={props.value === undefined || props.value === "" ? "" : String(props.value)}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
@@ -60,6 +60,21 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
}
}
}}
onBlur={(e) => {
// If input is empty, make 0 when focus is lost
if (e.target.value === "") {
const syntheticEvent = {
...e,
target: {
...e.target,
value: "0",
},
};
props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
);
}
}}
/>
);
},

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.20.4",
"version": "v0.20.5",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -93,6 +93,7 @@ export default async function handler(
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
@@ -107,6 +108,7 @@ export default async function handler(
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
),
});
@@ -151,6 +153,7 @@ export default async function handler(
eq(compose.branch, branchName),
eq(compose.repository, repository),
eq(compose.owner, owner),
eq(compose.githubId, githubResult.githubId),
),
});
@@ -240,6 +243,7 @@ export default async function handler(
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
),
with: {
previewDeployments: true,

View File

@@ -11,9 +11,13 @@ import {
createBackup,
findBackupById,
findMariadbByBackupId,
findMariadbById,
findMongoByBackupId,
findMongoById,
findMySqlByBackupId,
findMySqlById,
findPostgresByBackupId,
findPostgresById,
findServerById,
removeBackupById,
removeScheduleBackup,
@@ -26,6 +30,17 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { execAsync } from "@dokploy/server/utils/process/execAsync";
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import {
restoreMariadbBackup,
restoreMongoBackup,
restoreMySqlBackup,
restorePostgresBackup,
} from "@dokploy/server/utils/restore";
import { observable } from "@trpc/server/observable";
export const backupRouter = createTRPCRouter({
create: protectedProcedure
@@ -209,27 +224,136 @@ export const backupRouter = createTRPCRouter({
});
}
}),
listBackupFiles: protectedProcedure
.input(
z.object({
destinationId: z.string(),
search: z.string(),
}),
)
.query(async ({ input }) => {
try {
const destination = await findDestinationById(input.destinationId);
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const lastSlashIndex = input.search.lastIndexOf("/");
const baseDir =
lastSlashIndex !== -1
? input.search.slice(0, lastSlashIndex + 1)
: "";
const searchTerm =
lastSlashIndex !== -1
? input.search.slice(lastSlashIndex + 1)
: input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
const { stdout } = await execAsync(listCommand);
const files = stdout.split("\n").filter(Boolean);
const results = baseDir
? files.map((file) => `${baseDir}${file}`)
: files;
if (searchTerm) {
return results.filter((file) =>
file.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return results;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Error listing backup files",
cause: error,
});
}
}),
restoreBackupWithLogs: protectedProcedure
.meta({
openapi: {
enabled: false,
path: "/restore-backup-with-logs",
method: "POST",
override: true,
},
})
.input(
z.object({
databaseId: z.string(),
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
databaseName: z.string().min(1),
backupFile: z.string().min(1),
destinationId: z.string().min(1),
}),
)
.subscription(async ({ input }) => {
const destination = await findDestinationById(input.destinationId);
if (input.databaseType === "postgres") {
const postgres = await findPostgresById(input.databaseId);
return observable<string>((emit) => {
restorePostgresBackup(
postgres,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
if (input.databaseType === "mysql") {
const mysql = await findMySqlById(input.databaseId);
return observable<string>((emit) => {
restoreMySqlBackup(
mysql,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
if (input.databaseType === "mariadb") {
const mariadb = await findMariadbById(input.databaseId);
return observable<string>((emit) => {
restoreMariadbBackup(
mariadb,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
if (input.databaseType === "mongo") {
const mongo = await findMongoById(input.databaseId);
return observable<string>((emit) => {
restoreMongoBackup(
mongo,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
return true;
}),
});
// export const getAdminId = async (backupId: string) => {
// const backup = await findBackupById(backupId);
// if (backup.databaseType === "postgres" && backup.postgresId) {
// const postgres = await findPostgresById(backup.postgresId);
// return postgres.project.adminId;
// }
// if (backup.databaseType === "mariadb" && backup.mariadbId) {
// const mariadb = await findMariadbById(backup.mariadbId);
// return mariadb.project.adminId;
// }
// if (backup.databaseType === "mysql" && backup.mysqlId) {
// const mysql = await findMySqlById(backup.mysqlId);
// return mysql.project.adminId;
// }
// if (backup.databaseType === "mongo" && backup.mongoId) {
// const mongo = await findMongoById(backup.mongoId);
// return mongo.project.adminId;
// }
// return null;
// };