Compare commits

...

20 Commits

Author SHA1 Message Date
Mauricio Siu
6e28196b0e chore(package): bump version to v0.20.7 2025-03-18 21:36:39 -06:00
Mauricio Siu
18bacae175 Merge pull request #1507 from nb5p/fix-alpine-linux-compatibility
fix(server-setup): resolve Alpine Linux compatibility issues
2025-03-18 21:35:43 -06:00
Mauricio Siu
f2be5a378e Merge pull request #1522 from ensarkurrt/canary
fix(ui): Improve Numeric Input Handling in Swarm Cluster Settings, Traefik Port Mappings, and Email Notifications
2025-03-18 21:27:20 -06:00
Mauricio Siu
aef24296b9 Merge pull request #1531 from Dokploy/fix/loader-swarm
Fix/loader swarm
2025-03-18 21:18:17 -06:00
Mauricio Siu
7123b9b109 feat(cluster): add error handling in AddManager and AddWorker components
- Integrated error handling in AddManager and AddWorker components to display error messages using AlertBlock when data fetching fails.
- Updated API query hooks to include error and isError states for improved user feedback during data operations.
2025-03-18 21:17:11 -06:00
Mauricio Siu
891dc840f5 feat(cluster): enhance node management UI with loading indicators and improved tab content
- Added loading indicators in AddManager and AddWorker components to enhance user experience during data fetching.
- Updated AddNode component to include overflow handling for tab content.
- Renamed "Show Nodes" to "Show Swarm Nodes" in ShowNodesModal for clarity.
2025-03-18 21:11:50 -06:00
Mauricio Siu
172694be30 Merge pull request #1530 from Dokploy/feat/add-date-to-restore-item
feat(backup): enhance RestoreBackup component and API to include serv…
2025-03-18 00:49:02 -06:00
Mauricio Siu
ea6cfc9d29 feat(backup): enhance RestoreBackup component and API to include serverId
- Added serverId prop to RestoreBackup component for better context during backup restoration.
- Updated ShowBackups component to pass serverId from the Postgres object.
- Modified backup API to handle serverId, allowing remote execution of backup commands when specified.
- Improved file display in RestoreBackup for better user experience.
2025-03-18 00:47:50 -06:00
Mauricio Siu
4fa5e10789 chore(package): bump version to v0.20.6 2025-03-18 00:18:39 -06:00
Mauricio Siu
cb7fbb777c Merge pull request #1528 from Dokploy/1524-getting-502-bad-gateway
1524 getting 502 bad gateway
2025-03-18 00:15:12 -06:00
Mauricio Siu
6a388fe370 feat(domain): add validation for traefik.me domain IP address requirement
- Implemented a check to ensure an IP address is set for traefik.me domains in the AddDomain and AddDomainCompose components.
- Integrated a new API query to determine if traefik.me domains can be generated based on the server's IP address.
- Added user feedback through alert messages when the IP address is not configured.
2025-03-18 00:13:55 -06:00
Mauricio Siu
0722182650 feat(auth): implement user creation validation and IP update logic
- Added validation for user creation to check for existing admin presence and validate x-dokploy-token.
- Integrated public IP retrieval for user updates when not in cloud environment.
- Enhanced error handling with APIError for better feedback during user creation process.
2025-03-17 23:59:39 -06:00
Mauricio Siu
5e1095d199 Merge pull request #1526 from Dokploy/fix/mongo-db-button-deploy
refactor: improve code formatting and structure in ShowGeneralMongo c…
2025-03-17 23:18:18 -06:00
Mauricio Siu
c80a31e8c4 refactor: improve code formatting and structure in ShowGeneralMongo component
- Standardized indentation and formatting for better readability.
- Enhanced tooltip integration within button elements for improved user experience.
- Maintained functionality for deploying, reloading, starting, and stopping MongoDB instances while ensuring consistent code style.
2025-03-17 23:16:29 -06:00
Ensar Kurt
3cdf4c426c revert commit from #1513 2025-03-18 00:05:59 +03:00
Ensar Kurt
7cb184dc97 email notification port, last digit staying error fix 2025-03-17 23:48:17 +03:00
Ensar Kurt
fe57333f84 manage port inputs, default zero fix 2025-03-17 23:47:54 +03:00
Ensar Kurt
04fd77c3a9 replicas input cannot be zero and empty 2025-03-17 23:42:09 +03:00
Mauricio Siu
7c17cfb5c7 refactor: improve button structure and tooltip integration across dashboard components
- Refactored button components in the dashboard to enhance structure and readability.
- Integrated tooltips directly within button elements for better user experience.
- Updated tooltip descriptions for clarity across various database actions (Deploy, Reload, Start, Stop) for Redis, MySQL, PostgreSQL, and MariaDB.
- Ensured consistent formatting and improved code maintainability.
2025-03-16 20:52:57 -06:00
nb5p
2974a8183e fix(server-setup): resolve Alpine Linux compatibility issues with setup scripts
Resolves #1482
2025-03-16 15:37:28 +08:00
24 changed files with 1489 additions and 1270 deletions

View File

@@ -40,7 +40,7 @@ interface Props {
} }
const AddRedirectchema = z.object({ const AddRedirectchema = z.object({
replicas: z.number(), replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(), registryId: z.string(),
}); });
@@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
placeholder="1" placeholder="1"
{...field} {...field}
onChange={(e) => { onChange={(e) => {
field.onChange(Number(e.target.value)); const value = e.target.value;
field.onChange(value === "" ? 0 : Number(value));
}} }}
type="number" type="number"
value={field.value || ""}
/> />
</FormControl> </FormControl>

View File

@@ -42,6 +42,7 @@ import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react"; import { Dices } from "lucide-react";
import type z from "zod"; import type z from "zod";
import Link from "next/link";
type Domain = z.infer<typeof domain>; type Domain = z.infer<typeof domain>;
@@ -83,6 +84,13 @@ export const AddDomain = ({
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } = const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation(); api.domain.generateDomain.useMutation();
const { data: canGenerateTraefikMeDomains } =
api.domain.canGenerateTraefikMeDomains.useQuery({
serverId: application?.serverId || "",
});
console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains);
const form = useForm<Domain>({ const form = useForm<Domain>({
resolver: zodResolver(domain), resolver: zodResolver(domain),
defaultValues: { defaultValues: {
@@ -186,6 +194,21 @@ export const AddDomain = ({
name="host" name="host"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
{!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{application?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to make your traefik.me domain work.
</AlertBlock>
)}
<FormLabel>Host</FormLabel> <FormLabel>Host</FormLabel>
<div className="flex gap-2"> <div className="flex gap-2">
<FormControl> <FormControl>

View File

@@ -76,25 +76,27 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="default"
<Button isLoading={data?.applicationStatus === "running"}
variant="default" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={data?.applicationStatus === "running"} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Rocket className="size-4 mr-1" /> <div className="flex items-center">
Deploy <Rocket className="size-4 mr-1" />
</Button> Deploy
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p> <TooltipContent sideOffset={5} className="z-[60]">
Downloads the source code and performs a complete build <p>
</p> Downloads the source code and performs a complete build
</TooltipContent> </p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Application" title="Reload Application"
@@ -114,23 +116,25 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={isReloading}
variant="secondary" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isReloading} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<RefreshCcw className="size-4 mr-1" /> <div className="flex items-center">
Reload <RefreshCcw className="size-4 mr-1" />
</Button> Reload
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Reload the application without rebuilding it</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Reload the application without rebuilding it</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Rebuild Application" title="Rebuild Application"
@@ -149,25 +153,28 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={data?.applicationStatus === "running"}
variant="secondary" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={data?.applicationStatus === "running"} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Hammer className="size-4 mr-1" /> <div className="flex items-center">
Rebuild <Hammer className="size-4 mr-1" />
</Button> Rebuild
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p> <TooltipContent sideOffset={5} className="z-[60]">
Only rebuilds the application without downloading new code <p>
</p> Only rebuilds the application without downloading new
</TooltipContent> code
</TooltipPrimitive.Portal> </p>
</Tooltip> </TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
@@ -188,26 +195,28 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={isStarting}
variant="secondary" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isStarting} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<CheckCircle2 className="size-4 mr-1" /> <div className="flex items-center">
Start <CheckCircle2 className="size-4 mr-1" />
</Button> Start
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p> <TooltipContent sideOffset={5} className="z-[60]">
Start the application (requires a previous successful <p>
build) Start the application (requires a previous successful
</p> build)
</TooltipContent> </p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
) : ( ) : (
<DialogAction <DialogAction
@@ -226,23 +235,25 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="destructive"
<Button isLoading={isStopping}
variant="destructive" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isStopping} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Ban className="size-4 mr-1" /> <div className="flex items-center">
Stop <Ban className="size-4 mr-1" />
</Button> Stop
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Stop the currently running application</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Stop the currently running application</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
)} )}
</TooltipProvider> </TooltipProvider>

View File

@@ -42,6 +42,7 @@ import { domainCompose } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react"; import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import type z from "zod"; import type z from "zod";
import Link from "next/link";
type Domain = z.infer<typeof domainCompose>; type Domain = z.infer<typeof domainCompose>;
@@ -102,6 +103,11 @@ export const AddDomainCompose = ({
? api.domain.update.useMutation() ? api.domain.update.useMutation()
: api.domain.create.useMutation(); : api.domain.create.useMutation();
const { data: canGenerateTraefikMeDomains } =
api.domain.canGenerateTraefikMeDomains.useQuery({
serverId: compose?.serverId || "",
});
const form = useForm<Domain>({ const form = useForm<Domain>({
resolver: zodResolver(domainCompose), resolver: zodResolver(domainCompose),
defaultValues: { defaultValues: {
@@ -313,6 +319,21 @@ export const AddDomainCompose = ({
name="host" name="host"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
{!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{compose?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to make your traefik.me domain work.
</AlertBlock>
)}
<FormLabel>Host</FormLabel> <FormLabel>Host</FormLabel>
<div className="flex gap-2"> <div className="flex gap-2">
<FormControl> <FormControl>

View File

@@ -55,23 +55,25 @@ export const ComposeActions = ({ composeId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="default"
<Button isLoading={data?.composeStatus === "running"}
variant="default" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={data?.composeStatus === "running"} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Rocket className="size-4 mr-1" /> <div className="flex items-center">
Deploy <Rocket className="size-4 mr-1" />
</Button> Deploy
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Downloads the source code and performs a complete build</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Downloads the source code and performs a complete build</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Compose" title="Reload Compose"
@@ -90,23 +92,25 @@ export const ComposeActions = ({ composeId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={data?.composeStatus === "running"}
variant="secondary" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={data?.composeStatus === "running"} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<RefreshCcw className="size-4 mr-1" /> <div className="flex items-center">
Reload <RefreshCcw className="size-4 mr-1" />
</Button> Reload
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Reload the compose without rebuilding it</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Reload the compose without rebuilding it</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
{data?.composeType === "docker-compose" && {data?.composeType === "docker-compose" &&
data?.composeStatus === "idle" ? ( data?.composeStatus === "idle" ? (
@@ -127,25 +131,27 @@ export const ComposeActions = ({ composeId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={isStarting}
variant="secondary" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isStarting} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<CheckCircle2 className="size-4 mr-1" /> <div className="flex items-center">
Start <CheckCircle2 className="size-4 mr-1" />
</Button> Start
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p> <TooltipContent sideOffset={5} className="z-[60]">
Start the compose (requires a previous successful build) <p>
</p> Start the compose (requires a previous successful build)
</TooltipContent> </p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
) : ( ) : (
<DialogAction <DialogAction
@@ -164,23 +170,25 @@ export const ComposeActions = ({ composeId }: Props) => {
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="destructive"
<Button isLoading={isStopping}
variant="destructive" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isStopping} >
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Ban className="size-4 mr-1" /> <div className="flex items-center">
Stop <Ban className="size-4 mr-1" />
</Button> Stop
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Stop the currently running compose</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Stop the currently running compose</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
)} )}
</TooltipProvider> </TooltipProvider>

View File

@@ -48,6 +48,7 @@ import { toast } from "sonner";
interface Props { interface Props {
databaseId: string; databaseId: string;
databaseType: Exclude<ServiceType, "application" | "redis">; databaseType: Exclude<ServiceType, "application" | "redis">;
serverId: string | null;
} }
const RestoreBackupSchema = z.object({ const RestoreBackupSchema = z.object({
@@ -76,7 +77,11 @@ const RestoreBackupSchema = z.object({
type RestoreBackup = z.infer<typeof RestoreBackupSchema>; type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
export const RestoreBackup = ({ databaseId, databaseType }: Props) => { export const RestoreBackup = ({
databaseId,
databaseType,
serverId,
}: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -101,6 +106,7 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
{ {
destinationId: destionationId, destinationId: destionationId,
search, search,
serverId: serverId ?? "",
}, },
{ {
enabled: isOpen && !!destionationId, enabled: isOpen && !!destionationId,
@@ -304,7 +310,9 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
form.setValue("backupFile", file); form.setValue("backupFile", file);
}} }}
> >
{file} <div className="flex w-full justify-between">
<span>{file}</span>
</div>
<CheckIcon <CheckIcon
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",

View File

@@ -74,7 +74,11 @@ export const ShowBackups = ({ id, type }: Props) => {
{postgres && postgres?.backups?.length > 0 && ( {postgres && postgres?.backups?.length > 0 && (
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto"> <div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
<AddBackup databaseId={id} databaseType={type} refetch={refetch} /> <AddBackup databaseId={id} databaseType={type} refetch={refetch} />
<RestoreBackup databaseId={id} databaseType={type} /> <RestoreBackup
databaseId={id}
databaseType={type}
serverId={postgres.serverId}
/>
</div> </div>
)} )}
</CardHeader> </CardHeader>
@@ -108,7 +112,11 @@ export const ShowBackups = ({ id, type }: Props) => {
databaseType={type} databaseType={type}
refetch={refetch} refetch={refetch}
/> />
<RestoreBackup databaseId={id} databaseType={type} /> <RestoreBackup
databaseId={id}
databaseType={type}
serverId={postgres.serverId}
/>
</div> </div>
</div> </div>
) : ( ) : (

View File

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

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider disableHoverableContent={false}> <TooltipProvider disableHoverableContent={false}>
<DialogAction <DialogAction
title="Deploy Postgres" title="Deploy PostgreSQL"
description="Are you sure you want to deploy this postgres?" description="Are you sure you want to deploy this postgres?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -83,26 +83,28 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
refetch(); refetch();
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="default"
<Button isLoading={data?.applicationStatus === "running"}
variant="default" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={data?.applicationStatus === "running"} >
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Rocket className="size-4 mr-1" /> <div className="flex items-center">
Deploy <Rocket className="size-4 mr-1" />
</Button> Deploy
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Downloads and sets up the PostgreSQL database</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Downloads and sets up the PostgreSQL database</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Postgres" title="Reload PostgreSQL"
description="Are you sure you want to reload this postgres?" description="Are you sure you want to reload this postgres?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -111,35 +113,37 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName: data?.appName || "", appName: data?.appName || "",
}) })
.then(() => { .then(() => {
toast.success("Postgres reloaded successfully"); toast.success("PostgreSQL reloaded successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error reloading Postgres"); toast.error("Error reloading PostgreSQL");
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={isReloading}
variant="secondary" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isReloading} >
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<RefreshCcw className="size-4 mr-1" /> <div className="flex items-center">
Reload <RefreshCcw className="size-4 mr-1" />
</Button> Reload
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Reload the PostgreSQL without rebuilding it</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Restart the PostgreSQL service without rebuilding</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
<DialogAction <DialogAction
title="Start Postgres" title="Start PostgreSQL"
description="Are you sure you want to start this postgres?" description="Are you sure you want to start this postgres?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -147,69 +151,73 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
postgresId: postgresId, postgresId: postgresId,
}) })
.then(() => { .then(() => {
toast.success("Postgres started successfully"); toast.success("PostgreSQL started successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error starting Postgres"); toast.error("Error starting PostgreSQL");
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="secondary"
<Button isLoading={isStarting}
variant="secondary" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isStarting} >
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<CheckCircle2 className="size-4 mr-1" /> <div className="flex items-center">
Start <CheckCircle2 className="size-4 mr-1" />
</Button> Start
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p> <TooltipContent sideOffset={5} className="z-[60]">
Start the PostgreSQL database (requires a previous <p>
successful setup) Start the PostgreSQL database (requires a previous
</p> successful setup)
</TooltipContent> </p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
) : ( ) : (
<DialogAction <DialogAction
title="Stop Postgres" title="Stop PostgreSQL"
description="Are you sure you want to stop this postgres?" description="Are you sure you want to stop this postgres?"
onClick={async () => { onClick={async () => {
await stop({ await stop({
postgresId: postgresId, postgresId: postgresId,
}) })
.then(() => { .then(() => {
toast.success("Postgres stopped successfully"); toast.success("PostgreSQL stopped successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error stopping Postgres"); toast.error("Error stopping PostgreSQL");
}); });
}} }}
> >
<Tooltip> <Button
<TooltipTrigger asChild> variant="destructive"
<Button isLoading={isStopping}
variant="destructive" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
isLoading={isStopping} >
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2" <Tooltip>
> <TooltipTrigger asChild>
<Ban className="size-4 mr-1" /> <div className="flex items-center">
Stop <Ban className="size-4 mr-1" />
</Button> Stop
</TooltipTrigger> </div>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Stop the currently running PostgreSQL database</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Stop the currently running PostgreSQL database</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction> </DialogAction>
)} )}
</TooltipProvider> </TooltipProvider>
@@ -221,8 +229,19 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
variant="outline" variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<Terminal className="size-4 mr-1" /> <Tooltip>
Open Terminal <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> </Button>
</DockerTerminalModal> </DockerTerminalModal>
</CardContent> </CardContent>

View File

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

View File

@@ -56,10 +56,10 @@ export const AddNode = ({ serverId }: Props) => {
<TabsTrigger value="worker">Worker</TabsTrigger> <TabsTrigger value="worker">Worker</TabsTrigger>
<TabsTrigger value="manager">Manager</TabsTrigger> <TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="worker" className="pt-4"> <TabsContent value="worker" className="pt-4 overflow-hidden">
<AddWorker serverId={serverId} /> <AddWorker serverId={serverId} />
</TabsContent> </TabsContent>
<TabsContent value="manager" className="pt-4"> <TabsContent value="manager" className="pt-4 overflow-hidden">
<AddManager serverId={serverId} /> <AddManager serverId={serverId} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
import { import {
DialogDescription, DialogDescription,
@@ -6,7 +7,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon, Loader2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
interface Props { interface Props {
@@ -14,56 +15,66 @@ interface Props {
} }
export const AddManager = ({ serverId }: Props) => { export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId }); const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
serverId,
});
return ( return (
<> <>
<div> <CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0"> <DialogHeader>
<DialogHeader> <DialogTitle>Add a new manager</DialogTitle>
<DialogTitle>Add a new manager</DialogTitle> <DialogDescription>Add a new manager</DialogDescription>
<DialogDescription>Add a new manager</DialogDescription> </DialogHeader>
</DialogHeader> {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="flex flex-col gap-2.5 text-sm"> {isLoading ? (
<span>1. Go to your new server and run the following command</span> <Loader2 className="w-full animate-spin text-muted-foreground" />
<span className="bg-muted rounded-lg p-2 flex justify-between"> ) : (
curl https://get.docker.com | sh -s -- --version {data?.version} <>
<button <div className="flex flex-col gap-2.5 text-sm">
type="button" <span>
className="self-center" 1. Go to your new server and run the following command
onClick={() => { </span>
copy( <span className="bg-muted rounded-lg p-2 flex justify-between">
`curl https://get.docker.com | sh -s -- --version ${data?.version}`, curl https://get.docker.com | sh -s -- --version {data?.version}
); <button
toast.success("Copied to clipboard"); type="button"
}} className="self-center"
> onClick={() => {
<CopyIcon className="h-4 w-4 cursor-pointer" /> copy(
</button> `curl https://get.docker.com | sh -s -- --version ${data?.version}`,
</span> );
</div> toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<div className="flex flex-col gap-2.5 text-sm"> <div className="flex flex-col gap-2.5 text-sm">
<span> <span>
2. Run the following command to add the node(manager) to your 2. Run the following command to add the node(manager) to your
cluster cluster
</span> </span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command} <span className="bg-muted rounded-lg p-2 flex">
<button {data?.command}
type="button" <button
className="self-start" type="button"
onClick={() => { className="self-start"
copy(data?.command || ""); onClick={() => {
toast.success("Copied to clipboard"); copy(data?.command || "");
}} toast.success("Copied to clipboard");
> }}
<CopyIcon className="h-4 w-4 cursor-pointer" /> >
</button> <CopyIcon className="h-4 w-4 cursor-pointer" />
</span> </button>
</div> </span>
</CardContent> </div>
</div> </>
)}
</CardContent>
</> </>
); );
}; };

View File

@@ -17,7 +17,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
className="w-full cursor-pointer " className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()} onSelect={(e) => e.preventDefault()}
> >
Show Nodes Show Swarm Nodes
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen "> <DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
import { import {
DialogDescription, DialogDescription,
@@ -6,7 +7,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon, Loader2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
interface Props { interface Props {
@@ -14,54 +15,62 @@ interface Props {
} }
export const AddWorker = ({ serverId }: Props) => { export const AddWorker = ({ serverId }: Props) => {
const { data } = api.cluster.addWorker.useQuery({ serverId }); const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
serverId,
});
return ( return (
<div> <CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0"> <DialogHeader>
<DialogHeader> <DialogTitle>Add a new worker</DialogTitle>
<DialogTitle>Add a new worker</DialogTitle> <DialogDescription>Add a new worker</DialogDescription>
<DialogDescription>Add a new worker</DialogDescription> </DialogHeader>
</DialogHeader> {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="flex flex-col gap-2.5 text-sm"> {isLoading ? (
<span>1. Go to your new server and run the following command</span> <Loader2 className="w-full animate-spin text-muted-foreground" />
<span className="bg-muted rounded-lg p-2 flex justify-between"> ) : (
curl https://get.docker.com | sh -s -- --version {data?.version} <>
<button <div className="flex flex-col gap-2.5 text-sm">
type="button" <span>1. Go to your new server and run the following command</span>
className="self-center" <span className="bg-muted rounded-lg p-2 flex justify-between">
onClick={() => { curl https://get.docker.com | sh -s -- --version {data?.version}
copy( <button
`curl https://get.docker.com | sh -s -- --version ${data?.version}`, type="button"
); className="self-center"
toast.success("Copied to clipboard"); onClick={() => {
}} copy(
> `curl https://get.docker.com | sh -s -- --version ${data?.version}`,
<CopyIcon className="h-4 w-4 cursor-pointer" /> );
</button> toast.success("Copied to clipboard");
</span> }}
</div> >
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<div className="flex flex-col gap-2.5 text-sm"> <div className="flex flex-col gap-2.5 text-sm">
<span> <span>
2. Run the following command to add the node(worker) to your cluster 2. Run the following command to add the node(worker) to your
</span> cluster
</span>
<span className="bg-muted rounded-lg p-2 flex"> <span className="bg-muted rounded-lg p-2 flex">
{data?.command} {data?.command}
<button <button
type="button" type="button"
className="self-start" className="self-start"
onClick={() => { onClick={() => {
copy(data?.command || ""); copy(data?.command || "");
toast.success("Copied to clipboard"); toast.success("Copied to clipboard");
}} }}
> >
<CopyIcon className="h-4 w-4 cursor-pointer" /> <CopyIcon className="h-4 w-4 cursor-pointer" />
</button> </button>
</span> </span>
</div> </div>
</CardContent> </>
</div> )}
</CardContent>
); );
}; };

View File

@@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
{...field} {...field}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
if (value) { if (value === "") {
field.onChange(undefined);
} else {
const port = Number.parseInt(value); const port = Number.parseInt(value);
if (port > 0 && port < 65536) { if (port > 0 && port < 65536) {
field.onChange(port); field.onChange(port);
} }
} }
}} }}
value={field.value || ""}
type="number" type="number"
/> />
</FormControl> </FormControl>

View File

@@ -159,9 +159,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<Input <Input
type="number" type="number"
{...field} {...field}
onChange={(e) => onChange={(e) => {
field.onChange(Number(e.target.value)) const value = e.target.value;
} field.onChange(value === "" ? undefined : Number(value));
}}
value={field.value || ""}
className="w-full dark:bg-black" className="w-full dark:bg-black"
placeholder="e.g. 8080" placeholder="e.g. 8080"
/> />
@@ -185,9 +187,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<Input <Input
type="number" type="number"
{...field} {...field}
onChange={(e) => onChange={(e) => {
field.onChange(Number(e.target.value)) const value = e.target.value;
} field.onChange(value === "" ? undefined : Number(value));
}}
value={field.value || ""}
className="w-full dark:bg-black" className="w-full dark:bg-black"
placeholder="e.g. 80" placeholder="e.g. 80"
/> />

View File

@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
className={cn("text-left", className)} className={cn("text-left", className)}
ref={ref} ref={ref}
{...props} {...props}
value={props.value === undefined || props.value === "" ? "" : String(props.value)} value={props.value === undefined ? undefined : String(props.value)}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
if (value === "") { if (value === "") {
@@ -60,21 +60,6 @@ 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", "name": "dokploy",
"version": "v0.20.5", "version": "v0.20.7",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -31,7 +31,10 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { execAsync } from "@dokploy/server/utils/process/execAsync"; import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { getS3Credentials } from "@dokploy/server/utils/backups/utils"; import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
import { findDestinationById } from "@dokploy/server/services/destination"; import { findDestinationById } from "@dokploy/server/services/destination";
import { import {
@@ -229,6 +232,7 @@ export const backupRouter = createTRPCRouter({
z.object({ z.object({
destinationId: z.string(), destinationId: z.string(),
search: z.string(), search: z.string(),
serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
@@ -250,7 +254,16 @@ export const backupRouter = createTRPCRouter({
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath; const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
const { stdout } = await execAsync(listCommand); let stdout = "";
if (input.serverId) {
const result = await execAsyncRemote(listCommand, input.serverId);
stdout = result.stdout;
} else {
const result = await execAsync(listCommand);
stdout = result.stdout;
}
const files = stdout.split("\n").filter(Boolean); const files = stdout.split("\n").filter(Boolean);
const results = baseDir const results = baseDir

View File

@@ -13,7 +13,9 @@ import {
findDomainById, findDomainById,
findDomainsByApplicationId, findDomainsByApplicationId,
findDomainsByComposeId, findDomainsByComposeId,
findOrganizationById,
findPreviewDeploymentById, findPreviewDeploymentById,
findServerById,
generateTraefikMeDomain, generateTraefikMeDomain,
manageDomain, manageDomain,
removeDomain, removeDomain,
@@ -94,6 +96,19 @@ export const domainRouter = createTRPCRouter({
input.serverId, input.serverId,
); );
}), }),
canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() }))
.query(async ({ input, ctx }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
if (input.serverId) {
const server = await findServerById(input.serverId);
return server.ipAddress;
}
return organization?.owner.serverIp;
}),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateDomain) .input(apiUpdateDomain)

View File

@@ -8,6 +8,10 @@ import { db } from "../db";
import * as schema from "../db/schema"; import * as schema from "../db/schema";
import { sendEmail } from "../verification/send-verification-email"; import { sendEmail } from "../verification/send-verification-email";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { getPublicIpWithFallback } from "../wss/utils";
import { updateUser } from "../services/user";
import { getUserByToken } from "../services/admin";
import { APIError } from "better-auth/api";
const { handler, api } = betterAuth({ const { handler, api } = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
@@ -88,11 +92,40 @@ const { handler, api } = betterAuth({
databaseHooks: { databaseHooks: {
user: { user: {
create: { create: {
before: async (_user, context) => {
if (!IS_CLOUD) {
const xDokployToken =
context?.request?.headers?.get("x-dokploy-token");
if (xDokployToken) {
const user = await getUserByToken(xDokployToken);
if (!user) {
throw new APIError("BAD_REQUEST", {
message: "User not found",
});
}
} else {
const isAdminPresent = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
});
if (isAdminPresent) {
throw new APIError("BAD_REQUEST", {
message: "Admin is already created",
});
}
}
}
},
after: async (user) => { after: async (user) => {
const isAdminPresent = await db.query.member.findFirst({ const isAdminPresent = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"), where: eq(schema.member.role, "owner"),
}); });
if (!IS_CLOUD) {
await updateUser(user.id, {
serverIp: await getPublicIpWithFallback(),
});
}
if (IS_CLOUD || !isAdminPresent) { if (IS_CLOUD || !isAdminPresent) {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const organization = await tx const organization = await tx

View File

@@ -361,7 +361,7 @@ const installUtilities = () => `
alpine) alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null apk update >/dev/null
apk add curl wget git jq openssl >/dev/null apk add curl wget git jq openssl sudo unzip tar >/dev/null
;; ;;
ubuntu | debian | raspbian) ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null