mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-24 00:25:27 +02:00
refactor: update environment selector and API routes to utilize environmentId for service management; enhance UI with Badge component for production environments
This commit is contained in:
@@ -23,6 +23,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { toast } from "sonner";
|
||||
import { ChevronDownIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface Environment {
|
||||
environmentId: string;
|
||||
@@ -165,9 +166,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{currentEnv?.name || "Select Environment"}</span>
|
||||
{currentEnv?.name === "production" && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-green-100 text-green-800 rounded">
|
||||
<Badge >
|
||||
Prod
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
@@ -189,9 +190,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{environment.name}</span>
|
||||
{environment.name === "production" && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-green-100 text-green-800 rounded">
|
||||
<Badge >
|
||||
Prod
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{environment.environmentId === currentEnvironmentId && (
|
||||
@@ -241,7 +242,6 @@ export const AdvancedEnvironmentSelector = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Create Environment Dialog */}
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -285,9 +285,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateEnvironment}
|
||||
disabled={!name.trim() || createEnvironment.isPending}
|
||||
disabled={!name.trim() || createEnvironment.isLoading}
|
||||
>
|
||||
{createEnvironment.isPending ? "Creating..." : "Create"}
|
||||
{createEnvironment.isLoading ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -338,9 +338,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateEnvironment}
|
||||
disabled={!name.trim() || updateEnvironment.isPending}
|
||||
disabled={!name.trim() || updateEnvironment.isLoading}
|
||||
>
|
||||
{updateEnvironment.isPending ? "Updating..." : "Update"}
|
||||
{updateEnvironment.isLoading ? "Updating..." : "Update"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -370,9 +370,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteEnvironment}
|
||||
disabled={deleteEnvironment.isPending}
|
||||
disabled={deleteEnvironment.isLoading}
|
||||
>
|
||||
{deleteEnvironment.isPending ? "Deleting..." : "Delete"}
|
||||
{deleteEnvironment.isLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -275,12 +275,19 @@ const EnvironmentPage = (
|
||||
const { data: currentEnvironment } = api.environment.one.useQuery({
|
||||
environmentId,
|
||||
});
|
||||
const { data: allProjects } = api.project.all.useQuery();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
|
||||
const [selectedTargetProject, setSelectedTargetProject] =
|
||||
useState<string>("");
|
||||
const [selectedTargetEnvironment, setSelectedTargetEnvironment] =
|
||||
useState<string>("");
|
||||
|
||||
const { data: selectedProjectEnvironments } = api.environment.byProjectId.useQuery(
|
||||
{ projectId: selectedTargetProject },
|
||||
{ enabled: !!selectedTargetProject }
|
||||
);
|
||||
|
||||
const emptyServices =
|
||||
!currentEnvironment ||
|
||||
@@ -484,6 +491,10 @@ const EnvironmentPage = (
|
||||
toast.error("Please select a target project");
|
||||
return;
|
||||
}
|
||||
if (!selectedTargetEnvironment) {
|
||||
toast.error("Please select a target environment");
|
||||
return;
|
||||
}
|
||||
|
||||
let success = 0;
|
||||
setIsBulkActionLoading(true);
|
||||
@@ -492,50 +503,54 @@ const EnvironmentPage = (
|
||||
const service = filteredServices.find((s) => s.id === serviceId);
|
||||
if (!service) continue;
|
||||
|
||||
// TODO: Update move APIs to use targetEnvironmentId instead of targetProjectId
|
||||
switch (service.type) {
|
||||
case "application":
|
||||
await applicationActions.move.mutateAsync({
|
||||
applicationId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "compose":
|
||||
await composeActions.move.mutateAsync({
|
||||
composeId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "postgres":
|
||||
await postgresActions.move.mutateAsync({
|
||||
postgresId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "mysql":
|
||||
await mysqlActions.move.mutateAsync({
|
||||
mysqlId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "mariadb":
|
||||
await mariadbActions.move.mutateAsync({
|
||||
mariadbId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "redis":
|
||||
await redisActions.move.mutateAsync({
|
||||
redisId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
case "mongo":
|
||||
await mongoActions.move.mutateAsync({
|
||||
mongoId: serviceId,
|
||||
targetProjectId: selectedTargetProject,
|
||||
targetEnvironmentId: selectedTargetEnvironment,
|
||||
});
|
||||
break;
|
||||
}
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
});
|
||||
success++;
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
@@ -551,6 +566,9 @@ const EnvironmentPage = (
|
||||
setIsDropdownOpen(false);
|
||||
setIsMoveDialogOpen(false);
|
||||
setIsBulkActionLoading(false);
|
||||
// Reset move dialog state
|
||||
setSelectedTargetProject("");
|
||||
setSelectedTargetEnvironment("");
|
||||
};
|
||||
|
||||
const handleBulkDelete = async (deleteVolumes = false) => {
|
||||
@@ -964,14 +982,12 @@ const EnvironmentPage = (
|
||||
<DialogHeader>
|
||||
<DialogTitle>Move Services</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select the target project to move{" "}
|
||||
Select the target project and environment to move{" "}
|
||||
{selectedServices.length} services
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
{projectData?.environments?.filter(
|
||||
(p) => p.projectId !== projectId,
|
||||
).length === 0 ? (
|
||||
{allProjects?.filter((p) => p.projectId !== projectId).length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-4">
|
||||
<FolderInput className="h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
@@ -980,32 +996,70 @@ const EnvironmentPage = (
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={selectedTargetProject}
|
||||
onValueChange={setSelectedTargetProject}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allProjects
|
||||
?.filter((p) => p.projectId !== projectId)
|
||||
.map((project) => (
|
||||
<SelectItem
|
||||
key={project.projectId}
|
||||
value={project.projectId}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<>
|
||||
{/* Step 1: Select Project */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">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 !== projectId)
|
||||
.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="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">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>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsMoveDialogOpen(false)}
|
||||
onClick={() => {
|
||||
setIsMoveDialogOpen(false);
|
||||
setSelectedTargetProject("");
|
||||
setSelectedTargetEnvironment("");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -1013,9 +1067,9 @@ const EnvironmentPage = (
|
||||
onClick={handleBulkMove}
|
||||
isLoading={isBulkActionLoading}
|
||||
disabled={
|
||||
projectData?.environments?.filter(
|
||||
(p) => p.projectId !== projectId,
|
||||
).length === 0
|
||||
allProjects?.filter((p) => p.projectId !== projectId).length === 0 ||
|
||||
!selectedTargetProject ||
|
||||
!selectedTargetEnvironment
|
||||
}
|
||||
>
|
||||
Move Services
|
||||
|
||||
@@ -819,7 +819,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -833,11 +833,11 @@ export const applicationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -845,7 +845,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
const updatedApplication = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(applications.applicationId, input.applicationId))
|
||||
.returning()
|
||||
|
||||
@@ -655,7 +655,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -667,18 +667,18 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
const updatedCompose = await db
|
||||
.update(composeTable)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(composeTable.composeId, input.composeId))
|
||||
.returning()
|
||||
|
||||
@@ -337,7 +337,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
mariadbId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -349,11 +349,11 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
const updatedMariadb = await db
|
||||
.update(mariadbTable)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(mariadbTable.mariadbId, input.mariadbId))
|
||||
.returning()
|
||||
|
||||
@@ -351,7 +351,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
mongoId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -363,11 +363,11 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
const updatedMongo = await db
|
||||
.update(mongoTable)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(mongoTable.mongoId, input.mongoId))
|
||||
.returning()
|
||||
|
||||
@@ -346,7 +346,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
mysqlId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -358,11 +358,11 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
const updatedMysql = await db
|
||||
.update(mysqlTable)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(mysqlTable.mysqlId, input.mysqlId))
|
||||
.returning()
|
||||
|
||||
@@ -367,7 +367,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
postgresId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -381,11 +381,11 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const updatedPostgres = await db
|
||||
.update(postgresTable)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(postgresTable.postgresId, input.postgresId))
|
||||
.returning()
|
||||
|
||||
@@ -330,7 +330,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
redisId: z.string(),
|
||||
targetProjectId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -342,11 +342,11 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const targetProject = await findProjectById(input.targetProjectId);
|
||||
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
const targetEnvironment = await findEnvironmentById(input.targetEnvironmentId);
|
||||
if (targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this project",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ export const redisRouter = createTRPCRouter({
|
||||
const updatedRedis = await db
|
||||
.update(redisTable)
|
||||
.set({
|
||||
projectId: input.targetProjectId,
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(redisTable.redisId, input.redisId))
|
||||
.returning()
|
||||
|
||||
Reference in New Issue
Block a user