mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-17 13:15:23 +02:00
Compare commits
26 Commits
v0.26.2
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ec77ade8 | ||
|
|
5f441f5b54 | ||
|
|
ed1e3244c6 | ||
|
|
6685bd618e | ||
|
|
f5d334244a | ||
|
|
fd084c6d37 | ||
|
|
d8514b067b | ||
|
|
0590e78854 | ||
|
|
27fa0e881a | ||
|
|
72f2cc6268 | ||
|
|
854bd88e0a | ||
|
|
acf385a1f3 | ||
|
|
d1bc109697 | ||
|
|
38c7e1e996 | ||
|
|
54d5266573 | ||
|
|
3a5ac9d31f | ||
|
|
0ddf6b851f | ||
|
|
eb4fbff1b2 | ||
|
|
3aeb52810c | ||
|
|
8eaf2ab5c7 | ||
|
|
5ebcbf86ea | ||
|
|
67f4ca2cd9 | ||
|
|
6bb5404f87 | ||
|
|
3e356e6890 | ||
|
|
72cc7a2d2c | ||
|
|
d875e08d48 |
@@ -161,6 +161,50 @@ describe("helpers functions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Empty string variables", () => {
|
||||
it("should replace variables with empty string values correctly", () => {
|
||||
const variables = {
|
||||
smtp_username: "",
|
||||
smtp_password: "",
|
||||
non_empty: "value",
|
||||
};
|
||||
|
||||
const result1 = processValue("${smtp_username}", variables, mockSchema);
|
||||
expect(result1).toBe("");
|
||||
|
||||
const result2 = processValue("${smtp_password}", variables, mockSchema);
|
||||
expect(result2).toBe("");
|
||||
|
||||
const result3 = processValue("${non_empty}", variables, mockSchema);
|
||||
expect(result3).toBe("value");
|
||||
});
|
||||
|
||||
it("should not replace undefined variables", () => {
|
||||
const variables = {
|
||||
defined_var: "",
|
||||
};
|
||||
|
||||
const result = processValue("${undefined_var}", variables, mockSchema);
|
||||
expect(result).toBe("${undefined_var}");
|
||||
});
|
||||
|
||||
it("should handle mixed empty and non-empty variables in template", () => {
|
||||
const variables = {
|
||||
smtp_address: "smtp.example.com",
|
||||
smtp_port: "2525",
|
||||
smtp_username: "",
|
||||
smtp_password: "",
|
||||
};
|
||||
|
||||
const template =
|
||||
"SMTP_ADDRESS=${smtp_address} SMTP_PORT=${smtp_port} SMTP_USERNAME=${smtp_username} SMTP_PASSWORD=${smtp_password}";
|
||||
const result = processValue(template, variables, mockSchema);
|
||||
expect(result).toBe(
|
||||
"SMTP_ADDRESS=smtp.example.com SMTP_PORT=2525 SMTP_USERNAME= SMTP_PASSWORD=",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("${jwt}", () => {
|
||||
it("should generate a JWT string", () => {
|
||||
const jwt = processValue("${jwt}", {}, mockSchema);
|
||||
|
||||
@@ -42,9 +42,7 @@ const AddRegistrySchema = z.object({
|
||||
username: z.string().min(1, {
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
password: z.string(),
|
||||
registryUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -75,6 +73,7 @@ const AddRegistrySchema = z.object({
|
||||
),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
isEditing: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||
@@ -101,13 +100,21 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
const { mutateAsync, error, isError } = registryId
|
||||
? api.registry.update.useMutation()
|
||||
: api.registry.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: deployServers } = api.server.withSSHKey.useQuery();
|
||||
const { data: buildServers } = api.server.buildServers.useQuery();
|
||||
const servers = [...(deployServers || []), ...(buildServers || [])];
|
||||
const {
|
||||
mutateAsync: testRegistry,
|
||||
isLoading,
|
||||
error: testRegistryError,
|
||||
isError: testRegistryIsError,
|
||||
} = api.registry.testRegistry.useMutation();
|
||||
const {
|
||||
mutateAsync: testRegistryById,
|
||||
isLoading: isLoadingById,
|
||||
error: testRegistryByIdError,
|
||||
isError: testRegistryByIdIsError,
|
||||
} = api.registry.testRegistryById.useMutation();
|
||||
const form = useForm<AddRegistry>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
@@ -116,8 +123,26 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
imagePrefix: "",
|
||||
registryName: "",
|
||||
serverId: "",
|
||||
isEditing: !!registryId,
|
||||
},
|
||||
resolver: zodResolver(AddRegistrySchema),
|
||||
resolver: zodResolver(
|
||||
AddRegistrySchema.refine(
|
||||
(data) => {
|
||||
// When creating a new registry, password is required
|
||||
if (
|
||||
!data.isEditing &&
|
||||
(!data.password || data.password.length === 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Password is required",
|
||||
path: ["password"],
|
||||
},
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const password = form.watch("password");
|
||||
@@ -138,6 +163,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
registryUrl: registry.registryUrl,
|
||||
imagePrefix: registry.imagePrefix || "",
|
||||
registryName: registry.registryName,
|
||||
isEditing: true,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
@@ -146,13 +172,13 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
serverId: "",
|
||||
isEditing: false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, registry]);
|
||||
|
||||
const onSubmit = async (data: AddRegistry) => {
|
||||
await mutateAsync({
|
||||
password: data.password,
|
||||
const payload: any = {
|
||||
registryName: data.registryName,
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl || "",
|
||||
@@ -160,7 +186,15 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
registryId: registryId || "",
|
||||
})
|
||||
};
|
||||
|
||||
// Only include password if it's been provided (not empty)
|
||||
// When editing, empty password means "keep the existing password"
|
||||
if (data.password && data.password.length > 0) {
|
||||
payload.password = data.password;
|
||||
}
|
||||
|
||||
await mutateAsync(payload)
|
||||
.then(async (_data) => {
|
||||
await utils.registry.all.invalidate();
|
||||
toast.success(registryId ? "Registry updated" : "Registry added");
|
||||
@@ -198,11 +232,14 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
Fill the next fields to add a external registry.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{(isError || testRegistryIsError) && (
|
||||
{(isError || testRegistryIsError || testRegistryByIdIsError) && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{testRegistryError?.message || error?.message || ""}
|
||||
{testRegistryError?.message ||
|
||||
testRegistryByIdError?.message ||
|
||||
error?.message ||
|
||||
""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -253,10 +290,20 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>Password{registryId && " (Optional)"}</FormLabel>
|
||||
{registryId && (
|
||||
<FormDescription>
|
||||
Leave blank to keep existing password. Enter new
|
||||
password to test or update it.
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
placeholder={
|
||||
registryId
|
||||
? "Leave blank to keep existing"
|
||||
: "Password"
|
||||
}
|
||||
autoComplete="one-time-code"
|
||||
{...field}
|
||||
type="password"
|
||||
@@ -360,16 +407,33 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
<SelectValue placeholder="Select a server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deployServers && deployServers.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Deploy Servers</SelectLabel>
|
||||
{deployServers.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
{buildServers && buildServers.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Build Servers</SelectLabel>
|
||||
{buildServers.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
<SelectGroup>
|
||||
<SelectLabel>Servers</SelectLabel>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
@@ -387,8 +451,37 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoading || isLoadingById}
|
||||
onClick={async () => {
|
||||
// When editing with empty password, use the existing password from DB
|
||||
if (registryId && (!password || password.length === 0)) {
|
||||
await testRegistryById({
|
||||
registryId: registryId || "",
|
||||
...(serverId && { serverId }),
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error testing the registry");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// When creating, password is required
|
||||
if (!registryId && (!password || password.length === 0)) {
|
||||
form.setError("password", {
|
||||
type: "manual",
|
||||
message: "Password is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// When creating or editing with new password, validate and test with provided credentials
|
||||
const validationResult = AddRegistrySchema.safeParse({
|
||||
username,
|
||||
password,
|
||||
@@ -396,6 +489,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
registryName: "Dokploy Registry",
|
||||
imagePrefix,
|
||||
serverId,
|
||||
isEditing: !!registryId,
|
||||
});
|
||||
|
||||
if (!validationResult.success) {
|
||||
|
||||
@@ -122,6 +122,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
|
||||
await utils.destination.all.invalidate();
|
||||
if (destinationId) {
|
||||
await utils.destination.one.invalidate({ destinationId });
|
||||
}
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Activity } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { ShowStorageActions } from "./show-storage-actions";
|
||||
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||
@@ -13,20 +15,30 @@ import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
asButton?: boolean;
|
||||
}
|
||||
|
||||
export const ShowServerActions = ({ serverId }: Props) => {
|
||||
export const ShowServerActions = ({ serverId, asButton = false }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{asButton ? (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<Activity className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
View Actions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<div className="flex flex-col gap-1">
|
||||
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PlusIcon, Pencil } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -59,9 +59,10 @@ type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
asButton?: boolean;
|
||||
}
|
||||
|
||||
export const HandleServers = ({ serverId }: Props) => {
|
||||
export const HandleServers = ({ serverId, asButton = false }: Props) => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const utils = api.useUtils();
|
||||
@@ -137,21 +138,32 @@ export const HandleServers = ({ serverId }: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{serverId ? (
|
||||
{serverId ? (
|
||||
asButton ? (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
)
|
||||
) : (
|
||||
<DialogTrigger asChild>
|
||||
<Button className="cursor-pointer space-x-3">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Server
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-3xl ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{serverId ? "Edit" : "Create"} Server</DialogTitle>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon, ExternalLinkIcon, ServerIcon } from "lucide-react";
|
||||
import { CopyIcon, ExternalLinkIcon, ServerIcon, Settings } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -36,9 +36,10 @@ import { ValidateServer } from "./validate-server";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
asButton?: boolean;
|
||||
}
|
||||
|
||||
export const SetupServer = ({ serverId }: Props) => {
|
||||
export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: server } = api.server.one.useQuery(
|
||||
{
|
||||
@@ -81,14 +82,23 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{asButton ? (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Setup Server
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-4xl ">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { format } from "date-fns";
|
||||
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
|
||||
import {
|
||||
KeyIcon,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
ServerIcon,
|
||||
Clock,
|
||||
User,
|
||||
Key,
|
||||
Network,
|
||||
Terminal,
|
||||
Settings,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
@@ -24,14 +37,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||
import { TerminalModal } from "../web-server/terminal-modal";
|
||||
@@ -59,7 +69,7 @@ export const ShowServers = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{query?.success && isCloud && <WelcomeSuscription />}
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<Card className="h-full p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
@@ -114,240 +124,309 @@ export const ShowServers = () => {
|
||||
<HandleServers />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<Table>
|
||||
<TableCaption>
|
||||
<div className="flex flex-col gap-4">
|
||||
See all servers
|
||||
</div>
|
||||
</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-left">Name</TableHead>
|
||||
{isCloud && (
|
||||
<TableHead className="text-center">
|
||||
Status
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-center">
|
||||
Type
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
IP Address
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
Port
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
Username
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
SSH Key
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
Created
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
Actions
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((server) => {
|
||||
const canDelete = server.totalSum === 0;
|
||||
const isActive = server.serverStatus === "active";
|
||||
const isBuildServer =
|
||||
server.serverType === "build";
|
||||
return (
|
||||
<TableRow key={server.serverId}>
|
||||
<TableCell className="text-left">
|
||||
{server.name}
|
||||
</TableCell>
|
||||
{isCloud && (
|
||||
<TableHead className="text-center">
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{data?.map((server) => {
|
||||
const canDelete = server.totalSum === 0;
|
||||
const isActive = server.serverStatus === "active";
|
||||
const isBuildServer = server.serverType === "build";
|
||||
return (
|
||||
<Card
|
||||
key={server.serverId}
|
||||
className="relative hover:shadow-lg transition-shadow flex flex-col bg-transparent"
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ServerIcon className="size-5 text-muted-foreground" />
|
||||
<CardTitle className="text-lg">
|
||||
{server.name}
|
||||
</CardTitle>
|
||||
</div>
|
||||
{isActive &&
|
||||
server.sshKeyId &&
|
||||
!isBuildServer && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
More options
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
Advanced
|
||||
</DropdownMenuLabel>
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
{isCloud && (
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig?.server
|
||||
?.token
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ShowSwarmOverviewModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowNodesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowSchedulesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<div className="flex gap-2 mt-2 flex-wrap">
|
||||
{isCloud && (
|
||||
<>
|
||||
{server.serverStatus === "active" ? (
|
||||
<Badge variant="default">
|
||||
{server.serverStatus}
|
||||
</Badge>
|
||||
) : (
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-block">
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="cursor-help"
|
||||
>
|
||||
{server.serverStatus}
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="max-w-xs"
|
||||
side="bottom"
|
||||
>
|
||||
<p className="text-sm">
|
||||
This server is deactivated due
|
||||
to lack of payment. Please pay
|
||||
your invoice to reactivate it.
|
||||
If you think this is an error,
|
||||
please contact support.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Badge
|
||||
variant={
|
||||
server.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
isBuildServer
|
||||
? "secondary"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
{server.serverStatus}
|
||||
{server.serverType}
|
||||
</Badge>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
isBuildServer ? "secondary" : "default"
|
||||
}
|
||||
>
|
||||
{server.serverType}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 flex-1 flex flex-col">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Network className="size-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
IP:
|
||||
</span>
|
||||
<Badge variant="outline">
|
||||
{server.ipAddress}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge>{server.ipAddress}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{server.port}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{server.username}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-muted-foreground">
|
||||
Port:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{server.port}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<User className="size-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
User:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{server.username}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Key className="size-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
SSH Key:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{server.sshKeyId ? "Yes" : "No"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm pt-2 border-t">
|
||||
<Clock className="size-4 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Created{" "}
|
||||
{format(
|
||||
new Date(server.createdAt),
|
||||
"PPpp",
|
||||
"PPp",
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</div>
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
|
||||
{isActive && (
|
||||
<>
|
||||
{server.sshKeyId && (
|
||||
<TerminalModal
|
||||
serverId={server.serverId}
|
||||
>
|
||||
<span>
|
||||
{t(
|
||||
"settings.common.enterTerminal",
|
||||
)}
|
||||
</span>
|
||||
</TerminalModal>
|
||||
)}
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
|
||||
<HandleServers
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
|
||||
{server.sshKeyId &&
|
||||
!isBuildServer && (
|
||||
<ShowServerActions
|
||||
{/* Compact Actions */}
|
||||
{isActive && (
|
||||
<div className="flex items-center gap-2 pt-3 border-t mt-auto">
|
||||
<TooltipProvider>
|
||||
{server.sshKeyId && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<TerminalModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
asButton={true}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
</TerminalModal>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Terminal</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
canDelete
|
||||
? "Delete Server"
|
||||
: "Server has active services"
|
||||
}
|
||||
description={
|
||||
canDelete ? (
|
||||
"This will delete the server and all associated data"
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
You can not delete this server
|
||||
because it has active services.
|
||||
<AlertBlock type="warning">
|
||||
You have active services
|
||||
associated with this server,
|
||||
please delete them first.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
serverId: server.serverId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
`Server ${server.name} deleted successfully`,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Delete Server
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
|
||||
{isActive &&
|
||||
server.sshKeyId &&
|
||||
!isBuildServer && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>
|
||||
Extra
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Setup Server</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<HandleServers
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
{isCloud && (
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig
|
||||
?.server?.token
|
||||
}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit Server</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{server.sshKeyId && !isBuildServer && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Web Server Actions</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<ShowSwarmOverviewModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowNodesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
|
||||
<ShowSchedulesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
canDelete
|
||||
? "Delete Server"
|
||||
: "Server has active services"
|
||||
}
|
||||
description={
|
||||
canDelete ? (
|
||||
"This will delete the server and all associated data"
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
You can not delete this
|
||||
server because it has
|
||||
active services.
|
||||
<AlertBlock type="warning">
|
||||
You have active services
|
||||
associated with this
|
||||
server, please delete
|
||||
them first.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
serverId: server.serverId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
`Server ${server.name} deleted successfully`,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{canDelete
|
||||
? "Delete Server"
|
||||
: "Cannot delete - has active services"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
||||
{data && data?.length > 0 && (
|
||||
<div>
|
||||
<HandleServers />
|
||||
|
||||
@@ -24,10 +24,16 @@ const getTerminalKey = () => {
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
serverId: string;
|
||||
asButton?: boolean;
|
||||
}
|
||||
|
||||
export const TerminalModal = ({ children, serverId }: Props) => {
|
||||
export const TerminalModal = ({
|
||||
children,
|
||||
serverId,
|
||||
asButton = false,
|
||||
}: Props) => {
|
||||
const [terminalKey, setTerminalKey] = useState<string>(getTerminalKey());
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isLocalServer = serverId === "local";
|
||||
|
||||
const { data } = api.server.one.useQuery(
|
||||
@@ -43,15 +49,20 @@ export const TerminalModal = ({ children, serverId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
{asButton ? (
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent
|
||||
className="sm:max-w-7xl"
|
||||
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbPage,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
|
||||
interface Props {
|
||||
list: {
|
||||
interface BreadcrumbEntry {
|
||||
name: string;
|
||||
href?: string;
|
||||
dropdownItems?: {
|
||||
name: string;
|
||||
href?: string;
|
||||
href: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
list: BreadcrumbEntry[];
|
||||
}
|
||||
|
||||
export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
@@ -29,13 +43,29 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
{list.map((item, index) => (
|
||||
<Fragment key={item.name}>
|
||||
<BreadcrumbItem className="block">
|
||||
<BreadcrumbLink href={item?.href} asChild={!!item?.href}>
|
||||
{item.href ? (
|
||||
<Link href={item?.href}>{item?.name}</Link>
|
||||
) : (
|
||||
item?.name
|
||||
)}
|
||||
</BreadcrumbLink>
|
||||
{item.dropdownItems && item.dropdownItems.length > 0 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1 hover:text-foreground transition-colors outline-none">
|
||||
{item.name}
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{item.dropdownItems.map((subItem) => (
|
||||
<DropdownMenuItem key={subItem.href} asChild>
|
||||
<Link href={subItem.href}>{subItem.name}</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<BreadcrumbLink href={item?.href} asChild={!!item?.href}>
|
||||
{item.href ? (
|
||||
<Link href={item?.href}>{item?.name}</Link>
|
||||
) : (
|
||||
<BreadcrumbPage>{item?.name}</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{index + 1 < list.length && (
|
||||
<BreadcrumbSeparator className="block" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.26.2",
|
||||
"version": "v0.26.3",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -33,7 +33,7 @@
|
||||
"docker:build:canary": "./docker/build.sh canary",
|
||||
"docker:push:canary": "./docker/push.sh canary",
|
||||
"version": "echo $(node -p \"require('./package.json').version\")",
|
||||
"test": "vitest --config __test__/vitest.config.ts",
|
||||
"test": "vitest --config __test__/vitest.config.ts volume-backups",
|
||||
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -279,6 +279,16 @@ const EnvironmentPage = (
|
||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||
const { projectId, environmentId } = props;
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: projectId,
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
const [sortBy, setSortBy] = useState<string>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("servicesSort") || "lastDeploy-desc";
|
||||
@@ -863,6 +873,7 @@ const EnvironmentPage = (
|
||||
},
|
||||
{
|
||||
name: currentEnvironment.name,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -91,6 +91,15 @@ const Service = (
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.project?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="application" />
|
||||
@@ -98,11 +107,11 @@ const Service = (
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment.project.name || "",
|
||||
name: data?.environment?.project?.name || "",
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -80,6 +80,14 @@ const Service = (
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -92,7 +100,7 @@ const Service = (
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -62,6 +62,15 @@ const Mariadb = (
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="mariadb" />
|
||||
@@ -73,7 +82,7 @@ const Mariadb = (
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -61,6 +61,14 @@ const Mongo = (
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -73,7 +81,7 @@ const Mongo = (
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -60,6 +60,14 @@ const MySql = (
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -72,7 +80,7 @@ const MySql = (
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -60,6 +60,14 @@ const Postgresql = (
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -72,7 +80,7 @@ const Postgresql = (
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -60,6 +60,14 @@ const Redis = (
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||
projectId: data?.environment?.projectId || "",
|
||||
});
|
||||
const environmentDropdownItems =
|
||||
environments?.map((env) => ({
|
||||
name: env.name,
|
||||
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -72,7 +80,7 @@ const Redis = (
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
|
||||
@@ -87,7 +87,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
return true;
|
||||
return newMariadb;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
|
||||
@@ -87,7 +87,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
return true;
|
||||
return newMongo;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
|
||||
@@ -89,7 +89,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
return true;
|
||||
return newMysql;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
|
||||
@@ -91,7 +91,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
return true;
|
||||
return newPostgres;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
apiFindOneRegistry,
|
||||
apiRemoveRegistry,
|
||||
apiTestRegistry,
|
||||
apiTestRegistryById,
|
||||
apiUpdateRegistry,
|
||||
registry,
|
||||
} from "@/server/db/schema";
|
||||
@@ -109,6 +110,67 @@ export const registryRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error testing the registry",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
testRegistryById: protectedProcedure
|
||||
.input(apiTestRegistryById)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get the full registry with password from database
|
||||
const registryData = await db.query.registry.findFirst({
|
||||
where: eq(registry.registryId, input.registryId ?? ""),
|
||||
});
|
||||
|
||||
if (!registryData) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Registry not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (registryData.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to test this registry",
|
||||
});
|
||||
}
|
||||
|
||||
const args = [
|
||||
"login",
|
||||
registryData.registryUrl,
|
||||
"--username",
|
||||
registryData.username,
|
||||
"--password-stdin",
|
||||
];
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Select a server to test the registry",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.serverId && input.serverId !== "none") {
|
||||
await execAsyncRemote(
|
||||
input.serverId,
|
||||
`echo ${registryData.password} | docker ${args.join(" ")}`,
|
||||
);
|
||||
} else {
|
||||
await execFileAsync("docker", args, {
|
||||
input: Buffer.from(registryData.password).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -80,6 +80,14 @@ export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestRegistryById = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
})
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiRemoveRegistry = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
|
||||
@@ -42,7 +42,7 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
});
|
||||
|
||||
if (admin) {
|
||||
if (admin?.user) {
|
||||
return [
|
||||
...(admin.user.serverIp
|
||||
? [`http://${admin.user.serverIp}:3000`]
|
||||
|
||||
@@ -170,12 +170,12 @@ export function processValue(
|
||||
}
|
||||
|
||||
// If not a utility function, try to get from variables
|
||||
return variables[varName] || match;
|
||||
return varName in variables ? (variables[varName] ?? match) : match;
|
||||
});
|
||||
|
||||
// Then replace any remaining ${var} with their values from variables
|
||||
processedValue = processedValue.replace(/\${([^}]+)}/g, (match, varName) => {
|
||||
return variables[varName] || match;
|
||||
return varName in variables ? (variables[varName] ?? match) : match;
|
||||
});
|
||||
|
||||
return processedValue;
|
||||
|
||||
@@ -54,7 +54,7 @@ if [ "$REPLICA_STATUS" != "1" ]; then
|
||||
mongosh --eval '
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "localhost:27017", priority: 1 }]
|
||||
members: [{ _id: 0, host: "${appName}:27017", priority: 1 }]
|
||||
});
|
||||
|
||||
// Wait for the replica set to initialize
|
||||
|
||||
@@ -27,6 +27,9 @@ export const backupVolume = async (
|
||||
echo "Turning off volume backup: ${turnOff ? "Yes" : "No"}"
|
||||
echo "Starting volume backup"
|
||||
echo "Dir: ${volumeBackupPath}"
|
||||
echo "Ensuring ubuntu image is available..."
|
||||
docker image inspect ubuntu:latest > /dev/null 2>&1 || docker pull ubuntu:latest
|
||||
echo "Ubuntu image ready ✅"
|
||||
docker run --rm \
|
||||
-v ${volumeName}:/volume_data \
|
||||
-v ${volumeBackupPath}:/backup \
|
||||
|
||||
Reference in New Issue
Block a user