mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Add Duplicate Project functionality
- Introduced a new component for duplicating projects, allowing users to create a new project with the same configuration as an existing one. - Implemented a mutation in the project router to handle project duplication, including optional service duplication. - Updated the project detail page to include a dropdown menu for initiating the duplication process. - Enhanced the API to validate and process the duplication request, ensuring proper handling of services associated with the project.
This commit is contained in:
parent
d4925dd2b7
commit
2f16034cb0
214
apps/dokploy/components/dashboard/project/duplicate-project.tsx
Normal file
214
apps/dokploy/components/dashboard/project/duplicate-project.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import type { findProjectById } from "@dokploy/server";
|
||||||
|
import { Copy, Loader2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export type Services = {
|
||||||
|
appName: string;
|
||||||
|
serverId?: string | null;
|
||||||
|
name: string;
|
||||||
|
type:
|
||||||
|
| "mariadb"
|
||||||
|
| "application"
|
||||||
|
| "postgres"
|
||||||
|
| "mysql"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "compose";
|
||||||
|
description?: string | null;
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
status?: "idle" | "running" | "done" | "error";
|
||||||
|
};
|
||||||
|
|
||||||
|
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
||||||
|
|
||||||
|
interface DuplicateProjectProps {
|
||||||
|
project: Project;
|
||||||
|
services: Services[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DuplicateProject = ({
|
||||||
|
project,
|
||||||
|
services,
|
||||||
|
}: DuplicateProjectProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [includeServices, setIncludeServices] = useState(true);
|
||||||
|
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { mutateAsync: duplicateProject, isLoading } =
|
||||||
|
api.project.duplicate.useMutation({
|
||||||
|
onSuccess: async (newProject) => {
|
||||||
|
await utils.project.all.invalidate();
|
||||||
|
toast.success("Project duplicated successfully");
|
||||||
|
setOpen(false);
|
||||||
|
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDuplicate = async () => {
|
||||||
|
if (!name) {
|
||||||
|
toast.error("Project name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await duplicateProject({
|
||||||
|
sourceProjectId: project.projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
includeServices,
|
||||||
|
selectedServices: includeServices
|
||||||
|
? services
|
||||||
|
.filter((service) => selectedServices.includes(service.id))
|
||||||
|
.map((service) => ({
|
||||||
|
id: service.id,
|
||||||
|
type: service.type,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate Project
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
setOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
// Reset form when closing
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
setIncludeServices(true);
|
||||||
|
setSelectedServices([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Duplicate Project</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new project with the same configuration
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="New project name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Project description (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="includeServices"
|
||||||
|
checked={includeServices}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setIncludeServices(checked as boolean);
|
||||||
|
if (!checked) {
|
||||||
|
setSelectedServices([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="includeServices">Include services</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{includeServices && services.length > 0 && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Select services to duplicate</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{services.map((service) => (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={service.id}
|
||||||
|
checked={selectedServices.includes(service.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setSelectedServices((prev) =>
|
||||||
|
checked
|
||||||
|
? [...prev, service.id]
|
||||||
|
: prev.filter((id) => id !== service.id),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={service.id}>
|
||||||
|
{service.name} ({service.type})
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Duplicating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Duplicate"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -78,6 +78,7 @@ import {
|
|||||||
FolderInput,
|
FolderInput,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
MoreHorizontal,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
Trash2,
|
Trash2,
|
||||||
@ -92,6 +93,7 @@ import { useRouter } from "next/router";
|
|||||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@ -553,7 +555,7 @@ const Project = (
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>{data?.description}</CardDescription>
|
<CardDescription>{data?.description}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.role === "owner" || auth?.canCreateServices) && (
|
<div className="flex flex-row gap-4 flex-wrap justify-between items-center">
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
@ -593,8 +595,24 @@ const Project = (
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
{auth?.role === "owner" && (
|
||||||
|
<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">
|
||||||
|
<DuplicateProject
|
||||||
|
project={data}
|
||||||
|
services={applications}
|
||||||
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[60vh]">
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[60vh]">
|
||||||
|
@ -14,21 +14,44 @@ import {
|
|||||||
projects,
|
projects,
|
||||||
redis,
|
redis,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
addNewProject,
|
addNewProject,
|
||||||
checkProjectAccess,
|
checkProjectAccess,
|
||||||
|
createApplication,
|
||||||
|
createCompose,
|
||||||
|
createMariadb,
|
||||||
|
createMongo,
|
||||||
|
createMysql,
|
||||||
|
createPostgres,
|
||||||
createProject,
|
createProject,
|
||||||
|
createRedis,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
|
findApplicationById,
|
||||||
|
findComposeById,
|
||||||
|
findMongoById,
|
||||||
findMemberById,
|
findMemberById,
|
||||||
|
findRedisById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findUserById,
|
findUserById,
|
||||||
updateProjectById,
|
updateProjectById,
|
||||||
|
findPostgresById,
|
||||||
|
findMariadbById,
|
||||||
|
findMySqlById,
|
||||||
|
createDomain,
|
||||||
|
createPort,
|
||||||
|
createMount,
|
||||||
|
createRedirect,
|
||||||
|
createPreviewDeployment,
|
||||||
|
createBackup,
|
||||||
|
createSecurity,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, sql } from "drizzle-orm";
|
import { and, desc, eq, sql } from "drizzle-orm";
|
||||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const projectRouter = createTRPCRouter({
|
export const projectRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateProject)
|
.input(apiCreateProject)
|
||||||
@ -266,7 +289,317 @@ export const projectRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
duplicate: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
sourceProjectId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
includeServices: z.boolean().default(true),
|
||||||
|
selectedServices: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.enum([
|
||||||
|
"application",
|
||||||
|
"postgres",
|
||||||
|
"mariadb",
|
||||||
|
"mongo",
|
||||||
|
"mysql",
|
||||||
|
"redis",
|
||||||
|
"compose",
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
if (ctx.user.rol === "member") {
|
||||||
|
await checkProjectAccess(
|
||||||
|
ctx.user.id,
|
||||||
|
"create",
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get source project
|
||||||
|
const sourceProject = await findProjectById(input.sourceProjectId);
|
||||||
|
|
||||||
|
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to access this project",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new project
|
||||||
|
const newProject = await createProject(
|
||||||
|
{
|
||||||
|
name: input.name,
|
||||||
|
description: input.description,
|
||||||
|
env: sourceProject.env,
|
||||||
|
},
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (input.includeServices) {
|
||||||
|
const servicesToDuplicate = input.selectedServices || [];
|
||||||
|
|
||||||
|
// Helper function to duplicate a service
|
||||||
|
const duplicateService = async (id: string, type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case "application": {
|
||||||
|
const {
|
||||||
|
applicationId,
|
||||||
|
domains,
|
||||||
|
security,
|
||||||
|
ports,
|
||||||
|
registry,
|
||||||
|
redirects,
|
||||||
|
previewDeployments,
|
||||||
|
mounts,
|
||||||
|
...application
|
||||||
|
} = await findApplicationById(id);
|
||||||
|
|
||||||
|
const newApplication = await createApplication({
|
||||||
|
...application,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const domain of domains) {
|
||||||
|
const { domainId, ...rest } = domain;
|
||||||
|
await createDomain({
|
||||||
|
...rest,
|
||||||
|
applicationId: newApplication.applicationId,
|
||||||
|
domainType: "application",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const port of ports) {
|
||||||
|
const { portId, ...rest } = port;
|
||||||
|
await createPort({
|
||||||
|
...rest,
|
||||||
|
applicationId: newApplication.applicationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newApplication.applicationId,
|
||||||
|
serviceType: "application",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const redirect of redirects) {
|
||||||
|
const { redirectId, ...rest } = redirect;
|
||||||
|
await createRedirect({
|
||||||
|
...rest,
|
||||||
|
applicationId: newApplication.applicationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const secure of security) {
|
||||||
|
const { securityId, ...rest } = secure;
|
||||||
|
await createSecurity({
|
||||||
|
...rest,
|
||||||
|
applicationId: newApplication.applicationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const previewDeployment of previewDeployments) {
|
||||||
|
const { previewDeploymentId, ...rest } = previewDeployment;
|
||||||
|
await createPreviewDeployment({
|
||||||
|
...rest,
|
||||||
|
applicationId: newApplication.applicationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "postgres": {
|
||||||
|
const { postgresId, mounts, backups, ...postgres } =
|
||||||
|
await findPostgresById(id);
|
||||||
|
|
||||||
|
const newPostgres = await createPostgres({
|
||||||
|
...postgres,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newPostgres.postgresId,
|
||||||
|
serviceType: "postgres",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backup of backups) {
|
||||||
|
const { backupId, ...rest } = backup;
|
||||||
|
await createBackup({
|
||||||
|
...rest,
|
||||||
|
postgresId: newPostgres.postgresId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mariadb": {
|
||||||
|
const { mariadbId, mounts, backups, ...mariadb } =
|
||||||
|
await findMariadbById(id);
|
||||||
|
const newMariadb = await createMariadb({
|
||||||
|
...mariadb,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newMariadb.mariadbId,
|
||||||
|
serviceType: "mariadb",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backup of backups) {
|
||||||
|
const { backupId, ...rest } = backup;
|
||||||
|
await createBackup({
|
||||||
|
...rest,
|
||||||
|
mariadbId: newMariadb.mariadbId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mongo": {
|
||||||
|
const { mongoId, mounts, backups, ...mongo } =
|
||||||
|
await findMongoById(id);
|
||||||
|
const newMongo = await createMongo({
|
||||||
|
...mongo,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newMongo.mongoId,
|
||||||
|
serviceType: "mongo",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backup of backups) {
|
||||||
|
const { backupId, ...rest } = backup;
|
||||||
|
await createBackup({
|
||||||
|
...rest,
|
||||||
|
mongoId: newMongo.mongoId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mysql": {
|
||||||
|
const { mysqlId, mounts, backups, ...mysql } =
|
||||||
|
await findMySqlById(id);
|
||||||
|
const newMysql = await createMysql({
|
||||||
|
...mysql,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newMysql.mysqlId,
|
||||||
|
serviceType: "mysql",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backup of backups) {
|
||||||
|
const { backupId, ...rest } = backup;
|
||||||
|
await createBackup({
|
||||||
|
...rest,
|
||||||
|
mysqlId: newMysql.mysqlId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "redis": {
|
||||||
|
const { redisId, mounts, ...redis } = await findRedisById(id);
|
||||||
|
const newRedis = await createRedis({
|
||||||
|
...redis,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newRedis.redisId,
|
||||||
|
serviceType: "redis",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "compose": {
|
||||||
|
const { composeId, mounts, domains, ...compose } =
|
||||||
|
await findComposeById(id);
|
||||||
|
const newCompose = await createCompose({
|
||||||
|
...compose,
|
||||||
|
projectId: newProject.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mount of mounts) {
|
||||||
|
const { mountId, ...rest } = mount;
|
||||||
|
await createMount({
|
||||||
|
...rest,
|
||||||
|
serviceId: newCompose.composeId,
|
||||||
|
serviceType: "compose",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const domain of domains) {
|
||||||
|
const { domainId, ...rest } = domain;
|
||||||
|
await createDomain({
|
||||||
|
...rest,
|
||||||
|
composeId: newCompose.composeId,
|
||||||
|
domainType: "compose",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Duplicate selected services
|
||||||
|
|
||||||
|
for (const service of servicesToDuplicate) {
|
||||||
|
await duplicateService(service.id, service.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.user.rol === "member") {
|
||||||
|
await addNewProject(
|
||||||
|
ctx.user.id,
|
||||||
|
newProject.projectId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newProject;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Error duplicating the project: ${error instanceof Error ? error.message : error}`,
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
function buildServiceFilter(
|
function buildServiceFilter(
|
||||||
fieldName: AnyPgColumn,
|
fieldName: AnyPgColumn,
|
||||||
accessedServices: string[],
|
accessedServices: string[],
|
||||||
|
@ -139,7 +139,7 @@ const createSchema = createInsertSchema(compose, {
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
env: z.string().optional(),
|
env: z.string().optional(),
|
||||||
composeFile: z.string().min(1),
|
composeFile: z.string().optional(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
customGitSSHKeyId: z.string().optional(),
|
customGitSSHKeyId: z.string().optional(),
|
||||||
command: z.string().optional(),
|
command: z.string().optional(),
|
||||||
@ -155,6 +155,7 @@ export const apiCreateCompose = createSchema.pick({
|
|||||||
composeType: true,
|
composeType: true,
|
||||||
appName: true,
|
appName: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
|
composeFile: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateComposeByTemplate = createSchema
|
export const apiCreateComposeByTemplate = createSchema
|
||||||
|
@ -52,6 +52,7 @@ const createSchema = createInsertSchema(projects, {
|
|||||||
export const apiCreateProject = createSchema.pick({
|
export const apiCreateProject = createSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
env: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiFindOneProject = createSchema
|
export const apiFindOneProject = createSchema
|
||||||
|
@ -69,7 +69,7 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
...input,
|
...input,
|
||||||
composeFile: "",
|
composeFile: input.composeFile || "",
|
||||||
appName,
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
|
Loading…
Reference in New Issue
Block a user