mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973b54408e | ||
|
|
638fbe17a6 | ||
|
|
75e0d0ba78 | ||
|
|
0f1b911236 | ||
|
|
da148a6c50 | ||
|
|
c168648cce | ||
|
|
efd3ad4102 | ||
|
|
a9577395eb | ||
|
|
1d02d4308f | ||
|
|
e93e15a9c8 | ||
|
|
511a9beaa5 | ||
|
|
ac8c180ba6 | ||
|
|
9a2898ba4f | ||
|
|
2c258c84eb | ||
|
|
36511f34f8 | ||
|
|
fee5bbc535 | ||
|
|
84ad9a5bb3 | ||
|
|
69576988eb | ||
|
|
d65d050494 | ||
|
|
46a5adf793 | ||
|
|
5558ee3248 | ||
|
|
5d8ebd027e | ||
|
|
9aa49ce9be | ||
|
|
7bcfc17fe9 | ||
|
|
5d2d4104f0 | ||
|
|
2af8d6f565 | ||
|
|
ba45b27608 | ||
|
|
5aa1c6efb7 | ||
|
|
e9dad8f9f8 | ||
|
|
0c2a382541 | ||
|
|
81408985d4 | ||
|
|
d45b558251 | ||
|
|
a30b1a0cb8 | ||
|
|
c946e3e01b | ||
|
|
eb7bae2ef5 | ||
|
|
4533b193a4 | ||
|
|
c22f744e6c | ||
|
|
d3663eba6b | ||
|
|
f9e4a71144 | ||
|
|
985b8bc2e0 | ||
|
|
87ef889114 | ||
|
|
f3494922be | ||
|
|
27252cf58d | ||
|
|
f69fb7684b | ||
|
|
20a7995d73 | ||
|
|
6df66c3871 | ||
|
|
cbfdda1928 | ||
|
|
766279f265 | ||
|
|
f6e4ae700a | ||
|
|
8c8ffe04a7 | ||
|
|
f242f5c65e | ||
|
|
46348f43f6 | ||
|
|
c3476a1fdf | ||
|
|
379ea2ac65 | ||
|
|
a172abaee4 | ||
|
|
a325b293b6 | ||
|
|
364c2e192e | ||
|
|
d4a3c5cff9 | ||
|
|
229a9a3a5e | ||
|
|
5c1993a647 | ||
|
|
64a449a09d | ||
|
|
e65e6d225f | ||
|
|
b4fcdc433e | ||
|
|
5ce6172187 | ||
|
|
0affeea5dd | ||
|
|
8f76d520c8 | ||
|
|
566d9e0bee | ||
|
|
da858e215d | ||
|
|
f672c429c4 | ||
|
|
ce34fe3cd8 | ||
|
|
d8dbdb2b9e | ||
|
|
4065ad4428 | ||
|
|
e035062a10 | ||
|
|
36a1daae4b | ||
|
|
830a254837 | ||
|
|
b8580d69d6 | ||
|
|
e3f1518b0d | ||
|
|
ce19a42aee | ||
|
|
28a2ab9aa5 | ||
|
|
0535d780b1 | ||
|
|
e1dd666e24 | ||
|
|
ce71fa4f4d | ||
|
|
364d04f238 | ||
|
|
0db98c0b92 | ||
|
|
8410d94283 | ||
|
|
56cfd35e7d | ||
|
|
e7beb5c75b | ||
|
|
9c5a61e42f | ||
|
|
0ee5a6f13e | ||
|
|
1d35d218ca | ||
|
|
96cdffb5b9 | ||
|
|
353effd720 | ||
|
|
8bfe1632fa | ||
|
|
ed543e5397 | ||
|
|
c6892ba188 | ||
|
|
fa710d4855 | ||
|
|
375decebb2 | ||
|
|
be2e70a17e | ||
|
|
3b4214e040 | ||
|
|
43a493bb5a | ||
|
|
455cae6b8c | ||
|
|
869843d9ac | ||
|
|
d2b662f547 | ||
|
|
31336152ce | ||
|
|
6afd443257 | ||
|
|
1d023ac9f3 | ||
|
|
49616e53ea | ||
|
|
a32e934969 | ||
|
|
eb495b7b99 | ||
|
|
65ddc22010 | ||
|
|
7a5b9e3b76 | ||
|
|
5a302d3c47 | ||
|
|
5c5066bc72 | ||
|
|
228d12a61c | ||
|
|
fd0a9b8b58 | ||
|
|
1ea7d2e1bf | ||
|
|
7e08c8881e | ||
|
|
e68d867d31 | ||
|
|
a53929a787 | ||
|
|
ec8eaf6249 | ||
|
|
0d58935ad1 | ||
|
|
f7c8324c4b | ||
|
|
c0e9670daf | ||
|
|
a710728e77 | ||
|
|
80bd80b786 | ||
|
|
6e262cde0d | ||
|
|
21b2cce7a0 | ||
|
|
870e074825 | ||
|
|
055b59e6fa | ||
|
|
8c06296503 | ||
|
|
29ce8908ee | ||
|
|
0abf62dd52 | ||
|
|
e2b155280a | ||
|
|
9d1cf3736b | ||
|
|
3a95474662 | ||
|
|
3858205e52 | ||
|
|
06b8c82484 | ||
|
|
8ea453f444 | ||
|
|
63c0912849 | ||
|
|
6211a19805 | ||
|
|
8642d8235e | ||
|
|
b52f57cb0d | ||
|
|
d4d74d3831 | ||
|
|
9d497142db | ||
|
|
752c9f2818 | ||
|
|
577b126e66 | ||
|
|
be237ae4cf | ||
|
|
3080926a50 | ||
|
|
e3ee89104b | ||
|
|
f98f18b331 | ||
|
|
8505236263 | ||
|
|
763219e859 | ||
|
|
3fc5bfc5c5 | ||
|
|
813da8f811 | ||
|
|
5716954665 | ||
|
|
04d3eb9ec0 | ||
|
|
b592a025e4 | ||
|
|
8e5b0988cf | ||
|
|
7725b3ca36 | ||
|
|
ef89d05077 | ||
|
|
92c2a83d92 | ||
|
|
3decbd5207 | ||
|
|
8779c67b71 | ||
|
|
4dc7d9e3c8 | ||
|
|
a439286e5f |
@@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the swarm settings");
|
||||
toast.error("Error updating the swarm settings");
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -94,7 +94,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the command");
|
||||
toast.error("Error updating the command");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the command");
|
||||
toast.error("Error updating the command");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export const AddPort = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the port");
|
||||
toast.error("Error creating the port");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ export const DeletePort = ({ portId }: Props) => {
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
|
||||
toast.success("Port delete succesfully");
|
||||
toast.success("Port delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the port");
|
||||
toast.error("Error deleting the port");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -93,7 +93,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the port");
|
||||
toast.error("Error updating the port");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ export const AddRedirect = ({
|
||||
onDialogToggle(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the redirect");
|
||||
toast.error("Error creating the redirect");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ export const DeleteRedirect = ({ redirectId }: Props) => {
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
toast.success("Redirect delete succesfully");
|
||||
toast.success("Redirect delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the redirect");
|
||||
toast.error("Error deleting the redirect");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -88,7 +88,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the redirect");
|
||||
toast.error("Error updating the redirect");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export const AddSecurity = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the security");
|
||||
toast.error("Error creating security");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ export const DeleteSecurity = ({ securityId }: Props) => {
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
toast.success("Security delete succesfully");
|
||||
toast.success("Security delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the security");
|
||||
toast.error("Error deleting the security");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the security");
|
||||
toast.error("Error updating the security");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
const addResourcesApplication = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
@@ -72,7 +79,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -101,10 +108,25 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
@@ -120,7 +142,6 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -132,10 +153,25 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
@@ -163,21 +199,36 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
type="number"
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (
|
||||
value === "" ||
|
||||
/^[0-9]*\.?[0-9]*$/.test(value)
|
||||
) {
|
||||
const float = Number.parseFloat(value);
|
||||
field.onChange(float);
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -193,21 +244,36 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
{...field}
|
||||
type="number"
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (
|
||||
value === "" ||
|
||||
/^[0-9]*\.?[0-9]*$/.test(value)
|
||||
) {
|
||||
const float = Number.parseFloat(value);
|
||||
field.onChange(float);
|
||||
if (value === "") {
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -105,7 +105,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
form.reset();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
toast.error("Error updating the Traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ export const AddVolumes = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Bind mount");
|
||||
toast.error("Error creating the Bind mount");
|
||||
});
|
||||
} else if (data.type === "volume") {
|
||||
await mutateAsync({
|
||||
@@ -122,7 +122,7 @@ export const AddVolumes = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Volume mount");
|
||||
toast.error("Error creating the Volume mount");
|
||||
});
|
||||
} else if (data.type === "file") {
|
||||
await mutateAsync({
|
||||
@@ -138,7 +138,7 @@ export const AddVolumes = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the File mount");
|
||||
toast.error("Error creating the File mount");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ export const DeleteVolume = ({ mountId, refetch }: Props) => {
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Mount deleted succesfully");
|
||||
toast.success("Mount deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the mount");
|
||||
toast.error("Error deleting the mount");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -139,7 +139,7 @@ export const UpdateVolume = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Bind mount");
|
||||
toast.error("Error updating the Bind mount");
|
||||
});
|
||||
} else if (data.type === "volume") {
|
||||
await mutateAsync({
|
||||
@@ -153,7 +153,7 @@ export const UpdateVolume = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Volume mount");
|
||||
toast.error("Error updating the Volume mount");
|
||||
});
|
||||
} else if (data.type === "file") {
|
||||
await mutateAsync({
|
||||
@@ -168,7 +168,7 @@ export const UpdateVolume = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the File mount");
|
||||
toast.error("Error updating the File mount");
|
||||
});
|
||||
}
|
||||
refetch();
|
||||
|
||||
@@ -126,7 +126,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the build type");
|
||||
toast.error("Error saving the build type");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
||||
toast.success("Refresh updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the refresh token");
|
||||
toast.error("Error updating the refresh token");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -5,11 +7,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||
import { LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
|
||||
interface Props {
|
||||
logPath: string | null;
|
||||
@@ -19,26 +20,26 @@ interface Props {
|
||||
}
|
||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||
setAutoScroll(isAtBottom);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
@@ -69,20 +70,34 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
};
|
||||
}, [logPath, open]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const logs = parseLogs(data);
|
||||
setFilteredLogs(logs);
|
||||
}, [data]);
|
||||
let filteredLogsResult = logs;
|
||||
if (serverId) {
|
||||
let hideSubsequentLogs = false;
|
||||
filteredLogsResult = logs.filter((log) => {
|
||||
if (
|
||||
log.message.includes(
|
||||
"===================================EXTRA LOGS============================================",
|
||||
)
|
||||
) {
|
||||
hideSubsequentLogs = true;
|
||||
return showExtraLogs;
|
||||
}
|
||||
return showExtraLogs ? true : !hideSubsequentLogs;
|
||||
});
|
||||
}
|
||||
|
||||
setFilteredLogs(filteredLogsResult);
|
||||
}, [data, showExtraLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -103,28 +118,49 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deployment</DialogTitle>
|
||||
<DialogDescription>
|
||||
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge>
|
||||
<DialogDescription className="flex items-center gap-2">
|
||||
<span>
|
||||
See all the details of this deployment |{" "}
|
||||
<Badge variant="blank" className="text-xs">
|
||||
{filteredLogs.length} lines
|
||||
</Badge>
|
||||
</span>
|
||||
|
||||
{serverId && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="show-extra-logs"
|
||||
checked={showExtraLogs}
|
||||
onCheckedChange={(checked) =>
|
||||
setShowExtraLogs(checked as boolean)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="show-extra-logs"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Show Extra Logs
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
> {
|
||||
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
|
||||
<TerminalLine
|
||||
key={index}
|
||||
log={log}
|
||||
noTimestamp
|
||||
/>
|
||||
)) :
|
||||
(
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={index} log={log} noTimestamp />
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -105,8 +105,8 @@ export const AddDomain = ({
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
@@ -264,21 +264,21 @@ export const AddDomain = ({
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<FormLabel>Certificate Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
<SelectValue placeholder="Select a certificate provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
Let's Encrypt
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -57,10 +57,10 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Domain delete succesfully");
|
||||
toast.success("Domain delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete Domain");
|
||||
toast.error("Error deleting the Domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
toast.error("Error adding environment");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application deployed succesfully");
|
||||
toast.success("Application deployed successfully");
|
||||
await refetch();
|
||||
router.push(
|
||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||
@@ -59,7 +59,7 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
toast.error("Error deploying the Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
|
||||
@@ -137,7 +137,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Bitbucket provider");
|
||||
toast.error("Error saving the Bitbucket provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Docker provider");
|
||||
toast.error("Error saving the Docker provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the deployment");
|
||||
toast.error("Error saving the deployment");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Git provider");
|
||||
toast.error("Error saving the Git provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the github provider");
|
||||
toast.error("Error saving the github provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the gitlab provider");
|
||||
toast.error("Error saving the gitlab provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ResetApplication = ({ applicationId, appName }: Props) => {
|
||||
toast.success("Service Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the service");
|
||||
toast.error("Error reloading the service");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update Auto Deploy");
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -29,28 +31,67 @@ export const DockerLogs = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
export const badgeStateColor = (state: string) => {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "green";
|
||||
case "exited":
|
||||
case "shutdown":
|
||||
return "red";
|
||||
case "accepted":
|
||||
case "created":
|
||||
return "blue";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
},
|
||||
);
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
|
||||
const { data: services, isLoading: servicesLoading } =
|
||||
api.docker.getServiceContainersByAppName.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "swarm",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: containers, isLoading: containersLoading } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "native",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data?.length > 0) {
|
||||
setContainerId(data[0]?.containerId);
|
||||
if (option === "native") {
|
||||
if (containers && containers?.length > 0) {
|
||||
setContainerId(containers[0]?.containerId);
|
||||
}
|
||||
} else {
|
||||
if (services && services?.length > 0) {
|
||||
setContainerId(services[0]?.containerId);
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
}, [option, services, containers]);
|
||||
|
||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||
const containersLenght =
|
||||
option === "native" ? containers?.length : services?.length;
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
@@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Label>Select a container to view logs</Label>
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<Label>Select a container to view logs</Label>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{option === "native" ? "Native" : "Swarm"}
|
||||
</span>
|
||||
<Switch
|
||||
checked={option === "native"}
|
||||
onCheckedChange={(checked) => {
|
||||
setOption(checked ? "native" : "swarm");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
{isLoading ? (
|
||||
@@ -76,21 +131,45 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{data?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
{option === "native" ? (
|
||||
<div>
|
||||
{containers?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{services?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}@{container.node}
|
||||
)
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -105,8 +105,8 @@ export const AddPreviewDomain = ({
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
@@ -265,21 +265,21 @@ export const AddPreviewDomain = ({
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<FormLabel>Certificate Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
<SelectValue placeholder="Select a certificate provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
Let's Encrypt
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -18,15 +18,26 @@ import { ShowDeployment } from "../deployments/show-deployment";
|
||||
interface Props {
|
||||
deployments: RouterOutputs["deployment"]["all"];
|
||||
serverId?: string;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
|
||||
export const ShowPreviewBuilds = ({
|
||||
deployments,
|
||||
serverId,
|
||||
trigger,
|
||||
}: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">View Builds</Button>
|
||||
{trigger ? (
|
||||
trigger
|
||||
) : (
|
||||
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||
View Builds
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -8,30 +11,34 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { Pencil, RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
GitPullRequest,
|
||||
Layers,
|
||||
PenSquare,
|
||||
RocketIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowDeployment } from "../deployments/show-deployment";
|
||||
import Link from "next/link";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { AddPreviewDomain } from "./add-preview-domain";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||
import { ShowPreviewBuilds } from "./show-preview-builds";
|
||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const { data } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||
api.previewDeployment.delete.useMutation();
|
||||
|
||||
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||
api.previewDeployment.all.useQuery(
|
||||
{ applicationId },
|
||||
@@ -39,10 +46,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
// const [url, setUrl] = React.useState("");
|
||||
// useEffect(() => {
|
||||
// setUrl(document.location.origin);
|
||||
// }, []);
|
||||
|
||||
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||
deletePreviewDeployment({
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
})
|
||||
.then(() => {
|
||||
refetchPreviewDeployments();
|
||||
toast.success("Preview deployment deleted");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
@@ -65,7 +81,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
each pull request you create.
|
||||
</span>
|
||||
</div>
|
||||
{data?.previewDeployments?.length === 0 ? (
|
||||
{!previewDeployments?.length ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<RocketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
@@ -74,120 +90,131 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{previewDeployments?.map((previewDeployment) => {
|
||||
const { deployments, domain } = previewDeployment;
|
||||
|
||||
{previewDeployments?.map((deployment) => {
|
||||
const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`;
|
||||
const status = deployment.previewStatus;
|
||||
return (
|
||||
<div
|
||||
key={previewDeployment?.previewDeploymentId}
|
||||
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
|
||||
key={deployment.previewDeploymentId}
|
||||
className="group relative overflow-hidden border rounded-lg transition-colors"
|
||||
>
|
||||
<div className="flex justify-between gap-2 max-sm:flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
{deployments?.length === 0 ? (
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No deployments found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{previewDeployment?.pullRequestTitle}
|
||||
</span>
|
||||
<StatusTooltip
|
||||
status={previewDeployment.previewStatus}
|
||||
className="size-2.5"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{previewDeployment?.pullRequestTitle && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="break-all text-sm text-muted-foreground w-fit">
|
||||
Title: {previewDeployment?.pullRequestTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`absolute left-0 top-0 w-1 h-full ${
|
||||
status === "done"
|
||||
? "bg-green-500"
|
||||
: status === "running"
|
||||
? "bg-yellow-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{previewDeployment?.pullRequestURL && (
|
||||
<div className="flex items-center gap-2">
|
||||
<GithubIcon />
|
||||
<Link
|
||||
target="_blank"
|
||||
href={previewDeployment?.pullRequestURL}
|
||||
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
||||
>
|
||||
Pull Request URL
|
||||
</Link>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-sm">
|
||||
{deployment.pullRequestTitle}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
{deployment.branch}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ">
|
||||
<span>Domain </span>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`http://${domain?.host}`}
|
||||
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
||||
>
|
||||
{domain?.host}
|
||||
</Link>
|
||||
<AddPreviewDomain
|
||||
previewDeploymentId={
|
||||
previewDeployment.previewDeploymentId
|
||||
}
|
||||
domainId={domain?.domainId}
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AddPreviewDomain>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<StatusTooltip
|
||||
status={deployment.previewStatus}
|
||||
className="size-2"
|
||||
/>
|
||||
<DateTooltip date={deployment.createdAt} />
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
|
||||
{previewDeployment?.createdAt && (
|
||||
<div className="text-sm capitalize text-muted-foreground">
|
||||
<DateTooltip
|
||||
date={previewDeployment?.createdAt}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ShowPreviewBuilds
|
||||
deployments={previewDeployment?.deployments || []}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
<div className="pl-8 space-y-3">
|
||||
<div className="relative flex-grow">
|
||||
<Input
|
||||
value={deploymentUrl}
|
||||
readOnly
|
||||
className="pr-8 text-sm text-blue-500 hover:text-blue-600 cursor-pointer"
|
||||
onClick={() =>
|
||||
window.open(deploymentUrl, "_blank")
|
||||
}
|
||||
/>
|
||||
<ExternalLink className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<ShowModalLogs
|
||||
appName={previewDeployment.appName}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">View Logs</Button>
|
||||
</ShowModalLogs>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Preview"
|
||||
description="Are you sure you want to delete this preview?"
|
||||
onClick={() => {
|
||||
deletePreviewDeployment({
|
||||
previewDeploymentId:
|
||||
previewDeployment.previewDeploymentId,
|
||||
})
|
||||
.then(() => {
|
||||
refetchPreviewDeployments();
|
||||
toast.success("Preview deployment deleted");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
Delete Preview
|
||||
<div className="flex gap-2 opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() =>
|
||||
window.open(deployment.pullRequestURL, "_blank")
|
||||
}
|
||||
>
|
||||
<GithubIcon className="size-4" />
|
||||
Pull Request
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<ShowModalLogs
|
||||
appName={deployment.appName}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<FileText className="size-4" />
|
||||
Logs
|
||||
</Button>
|
||||
</ShowModalLogs>
|
||||
|
||||
<ShowPreviewBuilds
|
||||
deployments={deployment.deployments || []}
|
||||
serverId={data?.serverId || ""}
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Layers className="size-4" />
|
||||
Builds
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<AddPreviewDomain
|
||||
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||
domainId={deployment.domain?.domainId}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<PenSquare className="size-4" />
|
||||
</Button>
|
||||
</AddPreviewDomain>
|
||||
<DialogAction
|
||||
title="Delete Preview"
|
||||
description="Are you sure you want to delete this preview?"
|
||||
onClick={() =>
|
||||
handleDeletePreviewDeployment(
|
||||
deployment.previewDeploymentId,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isLoading={isLoading}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,12 +18,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { toast } from "sonner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -33,6 +26,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
env: z.string(),
|
||||
@@ -116,7 +117,10 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
<div>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">View Settings</Button>
|
||||
<Button variant="outline">
|
||||
<Settings2 className="size-4" />
|
||||
Configure
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||
<DialogHeader>
|
||||
@@ -218,21 +222,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
name="previewCertificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<FormLabel>Certificate Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
<SelectValue placeholder="Select a certificate provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
Let's Encrypt
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the application");
|
||||
toast.error("Error rebuilding the application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StartApplication = ({ applicationId }: Props) => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Application started succesfully");
|
||||
toast.success("Application started successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to start the Application");
|
||||
toast.error("Error starting the Application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StopApplication = ({ applicationId }: Props) => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Application stopped succesfully");
|
||||
toast.success("Application stopped successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to stop the Application");
|
||||
toast.error("Error stopping the Application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -76,14 +76,14 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
description: formData.description || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application updated succesfully");
|
||||
toast.success("Application updated successfully");
|
||||
utils.application.one.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the application");
|
||||
toast.error("Error updating the Application");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -81,7 +82,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the command");
|
||||
toast.error("Error updating the command");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
||||
<div>
|
||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||
<CardDescription>
|
||||
Append a custom command to the compose file
|
||||
Override a custom command to the compose file
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<AlertBlock type="warning">
|
||||
Modifying the default command may affect deployment stability,
|
||||
impacting logs and monitoring. Proceed carefully and test
|
||||
thoroughly. By default, the command starts with{" "}
|
||||
<strong>docker</strong>.
|
||||
</AlertBlock>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -32,6 +34,7 @@ const deleteComposeSchema = z.object({
|
||||
projectName: z.string().min(1, {
|
||||
message: "Compose name is required",
|
||||
}),
|
||||
deleteVolumes: z.boolean(),
|
||||
});
|
||||
|
||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||
@@ -51,6 +54,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
const form = useForm<DeleteCompose>({
|
||||
defaultValues: {
|
||||
projectName: "",
|
||||
deleteVolumes: false,
|
||||
},
|
||||
resolver: zodResolver(deleteComposeSchema),
|
||||
});
|
||||
@@ -58,7 +62,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
const onSubmit = async (formData: DeleteCompose) => {
|
||||
const expectedName = `${data?.name}/${data?.appName}`;
|
||||
if (formData.projectName === expectedName) {
|
||||
await mutateAsync({ composeId })
|
||||
const { deleteVolumes } = formData;
|
||||
await mutateAsync({ composeId, deleteVolumes })
|
||||
.then((result) => {
|
||||
push(`/dashboard/project/${result?.projectId}`);
|
||||
toast.success("Compose deleted successfully");
|
||||
@@ -133,6 +138,27 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="deleteVolumes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormLabel className="ml-2">
|
||||
Delete volumes associated with this compose
|
||||
</FormLabel>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const RefreshTokenCompose = ({ composeId }: Props) => {
|
||||
toast.success("Refresh Token updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the refresh token");
|
||||
toast.error("Error updating the refresh token");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -5,12 +7,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||
import { LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
|
||||
interface Props {
|
||||
logPath: string | null;
|
||||
@@ -26,25 +26,25 @@ export const ShowDeploymentCompose = ({
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||
setAutoScroll(isAtBottom);
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
@@ -76,19 +76,34 @@ export const ShowDeploymentCompose = ({
|
||||
};
|
||||
}, [logPath, open]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const logs = parseLogs(data);
|
||||
setFilteredLogs(logs);
|
||||
}, [data]);
|
||||
let filteredLogsResult = logs;
|
||||
if (serverId) {
|
||||
let hideSubsequentLogs = false;
|
||||
filteredLogsResult = logs.filter((log) => {
|
||||
if (
|
||||
log.message.includes(
|
||||
"===================================EXTRA LOGS============================================",
|
||||
)
|
||||
) {
|
||||
hideSubsequentLogs = true;
|
||||
return showExtraLogs;
|
||||
}
|
||||
return showExtraLogs ? true : !hideSubsequentLogs;
|
||||
});
|
||||
}
|
||||
|
||||
setFilteredLogs(filteredLogsResult);
|
||||
}, [data, showExtraLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
|
||||
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -109,32 +124,47 @@ export const ShowDeploymentCompose = ({
|
||||
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deployment</DialogTitle>
|
||||
<DialogDescription>
|
||||
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge>
|
||||
<DialogDescription className="flex items-center gap-2">
|
||||
<span>
|
||||
See all the details of this deployment |{" "}
|
||||
<Badge variant="blank" className="text-xs">
|
||||
{filteredLogs.length} lines
|
||||
</Badge>
|
||||
</span>
|
||||
{serverId && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="show-extra-logs"
|
||||
checked={showExtraLogs}
|
||||
onCheckedChange={(checked) =>
|
||||
setShowExtraLogs(checked as boolean)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="show-extra-logs"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Show Extra Logs
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
>
|
||||
|
||||
|
||||
{
|
||||
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
|
||||
<TerminalLine
|
||||
key={index}
|
||||
log={log}
|
||||
noTimestamp
|
||||
/>
|
||||
)) :
|
||||
(
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={index} log={log} noTimestamp />
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -127,8 +127,8 @@ export const AddDomainCompose = ({
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
@@ -400,21 +400,21 @@ export const AddDomainCompose = ({
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<FormLabel>Certificate Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
<SelectValue placeholder="Select a certificate provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
Let's Encrypt
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -70,7 +70,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
toast.error("Error adding environment");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update Auto Deploy");
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Error to update the compose config");
|
||||
toast.error("Error updating the Compose config");
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -53,11 +53,11 @@ export const DeployCompose = ({ composeId }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
router.push(
|
||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`
|
||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
toast.error("Error deploying Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
|
||||
@@ -139,7 +139,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Bitbucket provider");
|
||||
toast.error("Error saving the Bitbucket provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Git provider");
|
||||
toast.error("Error saving the Git provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the github provider");
|
||||
toast.error("Error saving the Github provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the gitlab provider");
|
||||
toast.error("Error saving the Gitlab provider");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
toast.success("Compose updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to randomize the compose");
|
||||
toast.error("Error randomizing the compose");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
toast.success("Compose randomized");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to randomize the compose");
|
||||
toast.error("Error randomizing the compose");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export const RedbuildCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the compose");
|
||||
toast.error("Error rebuilding the compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
toast.success("Fetched source type");
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Error to fetch source type", {
|
||||
toast.error("Error fetching source type", {
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,10 +53,10 @@ export const StopCompose = ({ composeId }: Props) => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Compose stopped succesfully");
|
||||
toast.success("Compose stopped successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to stop the compose");
|
||||
toast.error("Error stopping the compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
165
apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
Normal file
165
apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
(e) => e.DockerLogsId,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
badgeStateColor;
|
||||
|
||||
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
const { data: services, isLoading: servicesLoading } =
|
||||
api.docker.getStackContainersByAppName.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "swarm",
|
||||
},
|
||||
);
|
||||
|
||||
const { data: containers, isLoading: containersLoading } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
appType: "stack",
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName && option === "native",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (option === "native") {
|
||||
if (containers && containers?.length > 0) {
|
||||
setContainerId(containers[0]?.containerId);
|
||||
}
|
||||
} else {
|
||||
if (services && services?.length > 0) {
|
||||
setContainerId(services[0]?.containerId);
|
||||
}
|
||||
}
|
||||
}, [option, services, containers]);
|
||||
|
||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||
const containersLenght =
|
||||
option === "native" ? containers?.length : services?.length;
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Logs</CardTitle>
|
||||
<CardDescription>
|
||||
Watch the logs of the application in real time
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<Label>Select a container to view logs</Label>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{option === "native" ? "Native" : "Swarm"}
|
||||
</span>
|
||||
<Switch
|
||||
checked={option === "native"}
|
||||
onCheckedChange={(checked) => {
|
||||
setOption(checked ? "native" : "swarm");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{option === "native" ? (
|
||||
<div>
|
||||
{containers?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{services?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}@{container.node}
|
||||
)
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -87,7 +89,10 @@ export const ShowDockerLogsCompose = ({
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
@@ -97,6 +102,7 @@ export const ShowDockerLogsCompose = ({
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType="native"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StartCompose = ({ composeId }: Props) => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Compose started succesfully");
|
||||
toast.success("Compose started successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to start the Compose");
|
||||
toast.error("Error starting the Compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StopCompose = ({ composeId }: Props) => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Compose stopped succesfully");
|
||||
toast.success("Compose stopped successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to stop the Compose");
|
||||
toast.error("Error stopping the Compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -76,14 +76,14 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
||||
description: formData.description || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose updated succesfully");
|
||||
toast.success("Compose updated successfully");
|
||||
utils.compose.one.invalidate({
|
||||
composeId: composeId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Compose");
|
||||
toast.error("Error updating the Compose");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create a backup");
|
||||
toast.error("Error creating a backup");
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -46,10 +46,10 @@ export const DeleteBackup = ({ backupId, refetch }: Props) => {
|
||||
.then(() => {
|
||||
refetch();
|
||||
|
||||
toast.success("Backup delete succesfully");
|
||||
toast.success("Backup deleted successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the backup");
|
||||
toast.error("Error deleting the backup");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -109,7 +109,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the backup");
|
||||
toast.error("Error updating the Backup");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
runType: "swarm" | "native";
|
||||
}
|
||||
|
||||
export const priorities = [
|
||||
@@ -37,7 +38,11 @@ export const priorities = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
||||
export const DockerLogsId: React.FC<Props> = ({
|
||||
containerId,
|
||||
serverId,
|
||||
runType,
|
||||
}) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
@@ -104,6 +109,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
||||
tail: lines.toString(),
|
||||
since,
|
||||
search,
|
||||
runType,
|
||||
});
|
||||
|
||||
if (serverId) {
|
||||
|
||||
@@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({
|
||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
||||
<DockerLogsId
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
runType="native"
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
export const DockerLogsId = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
(e) => e.DockerLogsId,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
children?: React.ReactNode;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export const ShowDockerModalStackLogs = ({
|
||||
containerId,
|
||||
children,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>View Logs</DialogTitle>
|
||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
runType="swarm"
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -7,9 +7,10 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FancyAnsi } from "fancy-ansi";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import React from "react";
|
||||
import { type LogLine, getLogType, parseAnsi } from "./utils";
|
||||
import { type LogLine, getLogType } from "./utils";
|
||||
|
||||
interface LogLineProps {
|
||||
log: LogLine;
|
||||
@@ -17,6 +18,8 @@ interface LogLineProps {
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
const fancyAnsi = new FancyAnsi();
|
||||
|
||||
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
const { timestamp, message, rawTimestamp } = log;
|
||||
const { type, variant, color } = getLogType(message);
|
||||
@@ -34,37 +37,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
|
||||
const highlightMessage = (text: string, term: string) => {
|
||||
if (!term) {
|
||||
const segments = parseAnsi(text);
|
||||
return segments.map((segment, index) => (
|
||||
<span key={index} className={segment.className || undefined}>
|
||||
{segment.text}
|
||||
</span>
|
||||
));
|
||||
return (
|
||||
<span
|
||||
className="transition-colors"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: fancyAnsi.toHtml(text),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// For search, we need to handle both ANSI and search highlighting
|
||||
const segments = parseAnsi(text);
|
||||
return segments.map((segment, index) => {
|
||||
const parts = segment.text.split(
|
||||
new RegExp(`(${escapeRegExp(term)})`, "gi"),
|
||||
);
|
||||
return (
|
||||
<span key={index} className={segment.className || undefined}>
|
||||
{parts.map((part, partIndex) =>
|
||||
part.toLowerCase() === term.toLowerCase() ? (
|
||||
<span
|
||||
key={partIndex}
|
||||
className="bg-yellow-200 dark:bg-yellow-900"
|
||||
>
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
const htmlContent = fancyAnsi.toHtml(text);
|
||||
const modifiedContent = htmlContent.replace(
|
||||
/<span([^>]*)>([^<]*)<\/span>/g,
|
||||
(match, attrs, content) => {
|
||||
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
||||
if (!content.match(searchRegex)) return match;
|
||||
|
||||
const segments = content.split(searchRegex);
|
||||
const wrappedSegments = segments
|
||||
.map((segment: string) =>
|
||||
segment.toLowerCase() === term.toLowerCase()
|
||||
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
|
||||
: segment,
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<span${attrs}>${wrappedSegments}</span>`;
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: modifiedContent }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const tooltip = (color: string, timestamp: string | null) => {
|
||||
|
||||
@@ -12,47 +12,6 @@ interface LogStyle {
|
||||
variant: LogVariant;
|
||||
color: string;
|
||||
}
|
||||
interface AnsiSegment {
|
||||
text: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const ansiToTailwind: Record<number, string> = {
|
||||
// Reset
|
||||
0: "",
|
||||
// Regular colors
|
||||
30: "text-black dark:text-gray-900",
|
||||
31: "text-red-600 dark:text-red-500",
|
||||
32: "text-green-600 dark:text-green-500",
|
||||
33: "text-yellow-600 dark:text-yellow-500",
|
||||
34: "text-blue-600 dark:text-blue-500",
|
||||
35: "text-purple-600 dark:text-purple-500",
|
||||
36: "text-cyan-600 dark:text-cyan-500",
|
||||
37: "text-gray-600 dark:text-gray-400",
|
||||
// Bright colors
|
||||
90: "text-gray-500 dark:text-gray-600",
|
||||
91: "text-red-500 dark:text-red-600",
|
||||
92: "text-green-500 dark:text-green-600",
|
||||
93: "text-yellow-500 dark:text-yellow-600",
|
||||
94: "text-blue-500 dark:text-blue-600",
|
||||
95: "text-purple-500 dark:text-purple-600",
|
||||
96: "text-cyan-500 dark:text-cyan-600",
|
||||
97: "text-white dark:text-gray-300",
|
||||
// Background colors
|
||||
40: "bg-black",
|
||||
41: "bg-red-600",
|
||||
42: "bg-green-600",
|
||||
43: "bg-yellow-600",
|
||||
44: "bg-blue-600",
|
||||
45: "bg-purple-600",
|
||||
46: "bg-cyan-600",
|
||||
47: "bg-white",
|
||||
// Formatting
|
||||
1: "font-bold",
|
||||
2: "opacity-75",
|
||||
3: "italic",
|
||||
4: "underline",
|
||||
};
|
||||
|
||||
const LOG_STYLES: Record<LogType, LogStyle> = {
|
||||
error: {
|
||||
@@ -191,56 +150,3 @@ export const getLogType = (message: string): LogStyle => {
|
||||
|
||||
return LOG_STYLES.info;
|
||||
};
|
||||
|
||||
export function parseAnsi(text: string) {
|
||||
const segments: { text: string; className: string }[] = [];
|
||||
let currentIndex = 0;
|
||||
let currentClasses: string[] = [];
|
||||
|
||||
while (currentIndex < text.length) {
|
||||
const escStart = text.indexOf("\x1b[", currentIndex);
|
||||
|
||||
// No more escape sequences found
|
||||
if (escStart === -1) {
|
||||
if (currentIndex < text.length) {
|
||||
segments.push({
|
||||
text: text.slice(currentIndex),
|
||||
className: currentClasses.join(" "),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Add text before escape sequence
|
||||
if (escStart > currentIndex) {
|
||||
segments.push({
|
||||
text: text.slice(currentIndex, escStart),
|
||||
className: currentClasses.join(" "),
|
||||
});
|
||||
}
|
||||
|
||||
const escEnd = text.indexOf("m", escStart);
|
||||
if (escEnd === -1) break;
|
||||
|
||||
// Handle multiple codes in one sequence (e.g., \x1b[1;31m)
|
||||
const codesStr = text.slice(escStart + 2, escEnd);
|
||||
const codes = codesStr.split(";").map((c) => Number.parseInt(c, 10));
|
||||
|
||||
if (codes.includes(0)) {
|
||||
// Reset all formatting
|
||||
currentClasses = [];
|
||||
} else {
|
||||
// Add new classes for each code
|
||||
for (const code of codes) {
|
||||
const className = ansiToTailwind[code];
|
||||
if (className && !currentClasses.includes(className)) {
|
||||
currentClasses.push(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex = escEnd + 1;
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
@@ -59,7 +59,10 @@ export const DockerTerminalModal = ({
|
||||
{children}
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||
<DialogContent
|
||||
className="max-h-screen overflow-y-auto sm:max-w-7xl"
|
||||
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Docker Terminal</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -73,7 +76,7 @@ export const DockerTerminalModal = ({
|
||||
serverId={serverId || ""}
|
||||
/>
|
||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Are you sure you want to close the terminal?
|
||||
|
||||
@@ -86,7 +86,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
toast.error("Error updating the Traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ShowAdvancedMariadb = ({ mariadbId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -18,6 +18,13 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Tooltip,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -71,7 +78,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -100,28 +107,40 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -133,21 +152,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
@@ -166,21 +198,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
@@ -198,21 +243,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to Create the manual backup",
|
||||
"Error creating the manual backup",
|
||||
);
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -70,7 +70,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
toast.error("Error adding environment");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
||||
await deploy({
|
||||
mariadbId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
toast.error("Error deploying Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.message || "Error to deploy Database");
|
||||
toast.error(e.message || "Error deploying Database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ResetMariadb = ({ mariadbId, appName }: Props) => {
|
||||
toast.success("Service Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the service");
|
||||
toast.error("Error reloading the service");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the external port");
|
||||
toast.error("Error saving the external port");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StopMariadb = ({ mariadbId }: Props) => {
|
||||
await utils.mariadb.one.invalidate({
|
||||
mariadbId,
|
||||
});
|
||||
toast.success("Application stopped succesfully");
|
||||
toast.success("Application stopped successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to stop the Application");
|
||||
toast.error("Error stopping the Application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StartMariadb = ({ mariadbId }: Props) => {
|
||||
await utils.mariadb.one.invalidate({
|
||||
mariadbId,
|
||||
});
|
||||
toast.success("Database started succesfully");
|
||||
toast.success("Database started successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to start the Database");
|
||||
toast.error("Error starting the Database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -75,13 +75,13 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
description: formData.description || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("MariaDB updated succesfully");
|
||||
toast.success("MariaDB updated successfully");
|
||||
utils.mariadb.one.invalidate({
|
||||
mariadbId: mariadbId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Mariadb");
|
||||
toast.error("Error updating the Mariadb");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ShowAdvancedMongo = ({ mongoId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -19,6 +19,13 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Tooltip,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -71,7 +78,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -89,10 +96,6 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
the changes.
|
||||
</AlertBlock>
|
||||
<Form {...form}>
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the resources to
|
||||
apply the changes.
|
||||
</AlertBlock>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
@@ -104,28 +107,40 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -137,21 +152,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
@@ -170,21 +198,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
@@ -202,21 +243,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to Create the manual backup",
|
||||
"Error creating the manual backup",
|
||||
);
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -70,7 +70,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
toast.error("Error adding environment");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
||||
await deploy({
|
||||
mongoId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
toast.error("Error deploying Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.message || "Error to deploy Database");
|
||||
toast.error(e.message || "Error deploying Database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ResetMongo = ({ mongoId, appName }: Props) => {
|
||||
toast.success("Service Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the service");
|
||||
toast.error("Error reloading the service");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the external port");
|
||||
toast.error("Error saving the external port");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StopMongo = ({ mongoId }: Props) => {
|
||||
await utils.mongo.one.invalidate({
|
||||
mongoId,
|
||||
});
|
||||
toast.success("Application stopped succesfully");
|
||||
toast.success("Application stopped successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to stop the Application");
|
||||
toast.error("Error stopping the Application");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -49,10 +49,10 @@ export const StartMongo = ({ mongoId }: Props) => {
|
||||
await utils.mongo.one.invalidate({
|
||||
mongoId,
|
||||
});
|
||||
toast.success("Database started succesfully");
|
||||
toast.success("Database started successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to start the Database");
|
||||
toast.error("Error starting the Database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -76,14 +76,14 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
description: formData.description || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mongo updated succesfully");
|
||||
toast.success("Mongo updated successfully");
|
||||
utils.mongo.one.invalidate({
|
||||
mongoId: mongoId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update mongo database");
|
||||
toast.error("Error updating mongo database");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ShowAdvancedMysql = ({ mysqlId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,13 @@ import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Tooltip,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -71,7 +78,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -100,28 +107,40 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -133,21 +152,34 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
@@ -166,21 +198,34 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
@@ -198,21 +243,34 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to Create the manual backup",
|
||||
"Error creating the manual backup",
|
||||
);
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -70,7 +70,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
toast.error("Error adding environment");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ export const DeployMysql = ({ mysqlId }: Props) => {
|
||||
await deploy({
|
||||
mysqlId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
toast.error("Error deploying Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.message || "Error to deploy Database");
|
||||
toast.error(e.message || "Error deploying Database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ResetMysql = ({ mysqlId, appName }: Props) => {
|
||||
toast.success("Service Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the service");
|
||||
toast.error("Error reloading the service");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the external port");
|
||||
toast.error("Error saving the external port");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user