mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-20 06:35:22 +02:00
refactor: update DuplicateProject and AdvancedEnvironmentSelector components to utilize environmentId for improved context handling; enhance UI with project and environment selection features for better user experience
This commit is contained in:
@@ -31,13 +31,11 @@ import { findEnvironmentById } from "@dokploy/server";
|
||||
type Environment = Awaited<ReturnType<typeof findEnvironmentById>>;
|
||||
interface AdvancedEnvironmentSelectorProps {
|
||||
projectId: string;
|
||||
environments: Environment[];
|
||||
currentEnvironmentId?: string;
|
||||
}
|
||||
|
||||
export const AdvancedEnvironmentSelector = ({
|
||||
projectId,
|
||||
environments,
|
||||
currentEnvironmentId,
|
||||
}: AdvancedEnvironmentSelectorProps) => {
|
||||
const router = useRouter();
|
||||
@@ -46,6 +44,11 @@ export const AdvancedEnvironmentSelector = ({
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState<Environment | null>(null);
|
||||
|
||||
const { data: project } = api.project.one.useQuery({ projectId },{
|
||||
enabled: !!projectId,
|
||||
});
|
||||
const environments = project?.environments || [];
|
||||
|
||||
// Form states
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export type Services = {
|
||||
@@ -36,23 +43,32 @@ export type Services = {
|
||||
};
|
||||
|
||||
interface DuplicateProjectProps {
|
||||
projectId: string;
|
||||
environmentId: string;
|
||||
services: Services[];
|
||||
selectedServiceIds: string[];
|
||||
}
|
||||
|
||||
export const DuplicateProject = ({
|
||||
projectId,
|
||||
environmentId,
|
||||
services,
|
||||
selectedServiceIds,
|
||||
}: DuplicateProjectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "existing-environment"
|
||||
const [selectedTargetProject, setSelectedTargetProject] = useState<string>("");
|
||||
const [selectedTargetEnvironment, setSelectedTargetEnvironment] = useState<string>("");
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
|
||||
// Queries for project and environment selection
|
||||
const { data: allProjects } = api.project.all.useQuery();
|
||||
const { data: selectedProjectEnvironments } = api.environment.byProjectId.useQuery(
|
||||
{ projectId: selectedTargetProject },
|
||||
{ enabled: !!selectedTargetProject }
|
||||
);
|
||||
|
||||
const selectedServices = services.filter((service) =>
|
||||
selectedServiceIds.includes(service.id),
|
||||
);
|
||||
@@ -68,7 +84,7 @@ export const DuplicateProject = ({
|
||||
);
|
||||
setOpen(false);
|
||||
if (duplicateType === "new-project") {
|
||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||
router.push(`/dashboard/project/${newProject?.projectId}/environment/${newProject?.environmentId}`);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -82,8 +98,20 @@ export const DuplicateProject = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (duplicateType === "existing-environment") {
|
||||
if (!selectedTargetProject) {
|
||||
toast.error("Please select a target project");
|
||||
return;
|
||||
}
|
||||
if (!selectedTargetEnvironment) {
|
||||
toast.error("Please select a target environment");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update duplicate API to support targetProjectId and targetEnvironmentId
|
||||
await duplicateProject({
|
||||
sourceProjectId: projectId,
|
||||
sourceEnvironmentId: selectedTargetEnvironment,
|
||||
name,
|
||||
description,
|
||||
includeServices: true,
|
||||
@@ -105,6 +133,8 @@ export const DuplicateProject = ({
|
||||
setName("");
|
||||
setDescription("");
|
||||
setDuplicateType("new-project");
|
||||
setSelectedTargetProject("");
|
||||
setSelectedTargetEnvironment("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -127,7 +157,14 @@ export const DuplicateProject = ({
|
||||
<Label>Duplicate to</Label>
|
||||
<RadioGroup
|
||||
value={duplicateType}
|
||||
onValueChange={setDuplicateType}
|
||||
onValueChange={(value) => {
|
||||
setDuplicateType(value);
|
||||
// Reset selections when changing type
|
||||
if (value !== "existing-environment") {
|
||||
setSelectedTargetProject("");
|
||||
setSelectedTargetEnvironment("");
|
||||
}
|
||||
}}
|
||||
className="grid gap-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -135,8 +172,8 @@ export const DuplicateProject = ({
|
||||
<Label htmlFor="new-project">New project</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="same-project" id="same-project" />
|
||||
<Label htmlFor="same-project">Same project</Label>
|
||||
<RadioGroupItem value="existing-environment" id="existing-environment" />
|
||||
<Label htmlFor="existing-environment">Existing environment</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
@@ -165,6 +202,73 @@ export const DuplicateProject = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{duplicateType === "existing-environment" && (
|
||||
<>
|
||||
{allProjects?.filter((p) => p.projectId !== environmentId).length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No other projects available. Create a new project first.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Step 1: Select Project */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Target Project</Label>
|
||||
<Select
|
||||
value={selectedTargetProject}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTargetProject(value);
|
||||
setSelectedTargetEnvironment(""); // Reset environment when project changes
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allProjects
|
||||
?.filter((p) => p.projectId !== environmentId)
|
||||
.map((project) => (
|
||||
<SelectItem
|
||||
key={project.projectId}
|
||||
value={project.projectId}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Select Environment (only show if project is selected) */}
|
||||
{selectedTargetProject && (
|
||||
<div className="grid gap-2">
|
||||
<Label>Target Environment</Label>
|
||||
<Select
|
||||
value={selectedTargetEnvironment}
|
||||
onValueChange={setSelectedTargetEnvironment}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target environment" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectedProjectEnvironments?.map((env) => (
|
||||
<SelectItem
|
||||
key={env.environmentId}
|
||||
value={env.environmentId}
|
||||
>
|
||||
{env.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Selected services to duplicate</Label>
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||
@@ -187,18 +291,25 @@ export const DuplicateProject = ({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||
<Button
|
||||
onClick={handleDuplicate}
|
||||
disabled={
|
||||
isLoading ||
|
||||
(duplicateType === "new-project" && !name) ||
|
||||
(duplicateType === "existing-environment" && (!selectedTargetProject || !selectedTargetEnvironment))
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{duplicateType === "new-project"
|
||||
? "Duplicating project..."
|
||||
: "Duplicating services..."}
|
||||
? "Duplicating to new project..."
|
||||
: "Duplicating to environment..."}
|
||||
</>
|
||||
) : duplicateType === "new-project" ? (
|
||||
"Duplicate project"
|
||||
"Duplicate to new project"
|
||||
) : (
|
||||
"Duplicate services"
|
||||
"Duplicate to environment"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -790,7 +790,6 @@ const EnvironmentPage = (
|
||||
</ProjectEnvironment>
|
||||
<AdvancedEnvironmentSelector
|
||||
projectId={projectId}
|
||||
environments={projectData?.environments || []}
|
||||
currentEnvironmentId={environmentId}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
@@ -957,7 +956,7 @@ const EnvironmentPage = (
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
deleteProject,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
@@ -80,7 +81,7 @@ export const projectRouter = createTRPCRouter({
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
project.project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
@@ -312,7 +313,7 @@ export const projectRouter = createTRPCRouter({
|
||||
duplicate: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
sourceProjectId: z.string(),
|
||||
sourceEnvironmentId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
includeServices: z.boolean().default(true),
|
||||
@@ -346,9 +347,10 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Get source project
|
||||
const sourceProject = await findProjectById(input.sourceProjectId);
|
||||
const sourceEnvironment = input.duplicateInSameProject ? await findEnvironmentById(input.sourceEnvironmentId) : null;
|
||||
|
||||
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
|
||||
if (input.duplicateInSameProject &&sourceEnvironment?.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
@@ -357,15 +359,18 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
// Create new project or use existing one
|
||||
const targetProject = input.duplicateInSameProject
|
||||
? sourceProject
|
||||
? sourceEnvironment
|
||||
: await createProject(
|
||||
{
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
env: sourceProject.env,
|
||||
env: sourceEnvironment?.project.env,
|
||||
},
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
).then((value) => value.environment);
|
||||
|
||||
console.log("targetProject", targetProject);
|
||||
|
||||
|
||||
if (input.includeServices) {
|
||||
const servicesToDuplicate = input.selectedServices || [];
|
||||
@@ -398,7 +403,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${application.name} (copy)`
|
||||
: application.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const domain of domains) {
|
||||
@@ -468,7 +473,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${postgres.name} (copy)`
|
||||
: postgres.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -504,7 +509,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${mariadb.name} (copy)`
|
||||
: mariadb.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -540,7 +545,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${mongo.name} (copy)`
|
||||
: mongo.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -576,7 +581,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${mysql.name} (copy)`
|
||||
: mysql.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -612,7 +617,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${redis.name} (copy)`
|
||||
: redis.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -647,7 +652,7 @@ export const projectRouter = createTRPCRouter({
|
||||
name: input.duplicateInSameProject
|
||||
? `${compose.name} (copy)`
|
||||
: compose.name,
|
||||
projectId: targetProject.projectId,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -682,7 +687,7 @@ export const projectRouter = createTRPCRouter({
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
targetProject.projectId,
|
||||
targetProject?.projectId || "",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,14 +36,13 @@ export const createProject = async (
|
||||
}
|
||||
|
||||
// Automatically create a production environment
|
||||
try {
|
||||
await createProductionEnvironment(newProject.projectId);
|
||||
} catch (error) {
|
||||
console.error("Error creating production environment:", error);
|
||||
// Don't fail project creation if environment creation fails
|
||||
}
|
||||
const newEnvironment = await createProductionEnvironment(newProject.projectId);
|
||||
return {
|
||||
project: newProject,
|
||||
environment: newEnvironment,
|
||||
};
|
||||
|
||||
|
||||
return newProject;
|
||||
};
|
||||
|
||||
export const findProjectById = async (projectId: string) => {
|
||||
|
||||
Reference in New Issue
Block a user