mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-28 15:52:16 +00:00
3ca5afd49f
* feat: update dependencies and enhance UI components in Dokploy - Updated various package versions in pnpm-lock.yaml to improve compatibility and performance, including React and Tailwind CSS. - Modified components.json to change the styling to "radix-nova" and added new properties for icon library and menu color. - Refactored multiple components to use updated class names for better styling consistency and responsiveness. - Removed unused Radix UI components from package.json to streamline dependencies. - Adjusted layout and styling in several dashboard components for improved user experience and visual appeal. - Enhanced tooltip and form item components for better accessibility and usability. * refactor: enhance UI components in HandleCertificate and SidebarLogo - Updated the HandleCertificate component to improve dialog content height and textarea styling for better usability. - Adjusted the SidebarLogo component layout to enhance alignment and spacing, ensuring a more consistent appearance across the sidebar. - Implemented responsive design adjustments to textarea elements, preventing overflow and improving user experience. * refactor: improve UI consistency and styling across dashboard components - Updated Card components in ShowDeployments, ShowSchedules, and ShowVolumeBackups to remove unnecessary border styles for a cleaner look. - Enhanced TabsList components in ShowProviderForm and ShowProviderFormCompose by adding a variant for improved visual distinction. - Adjusted spacing in AdvancedEnvironmentSelector for better layout and readability. - Removed redundant border classes in Service component for a more streamlined design. * [autofix.ci] apply automated fixes * chore: update package dependencies and refactor email rendering - Upgraded React and React DOM to version 19.2.7 across multiple packages for improved performance and compatibility. - Updated TSX to version 4.22.4 in various package.json files. - Refactored email rendering from `renderAsync` to `render` in notification utilities and email templates for consistency. - Adjusted Tailwind configuration usage in email templates to utilize a centralized configuration file. These changes enhance the overall stability and maintainability of the codebase. * refactor: update badge variants across dashboard components - Changed badge variant from "outline-solid" to "outline" in multiple components including columns, show-domains, and various deployment tables for consistency in styling. - Updated button variants in billing and project templates to align with the new badge styling. - Enhanced input components to support password generation and error messaging, improving user experience. These changes streamline the UI and ensure a cohesive design across the application. * refactor: clean up calendar component imports and structure - Removed redundant imports of icons from lucide-react and streamlined the import statements for better readability. - Adjusted the placement of type imports to enhance code organization. These changes improve the maintainability and clarity of the calendar component. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
439 lines
13 KiB
TypeScript
439 lines
13 KiB
TypeScript
import type { findEnvironmentsByProjectId } from "@dokploy/server";
|
|
import { ChevronDownIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-react";
|
|
import { useRouter } from "next/router";
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { AlertBlock } from "@/components/shared/alert-block";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { api } from "@/utils/api";
|
|
|
|
type Environment = Awaited<
|
|
ReturnType<typeof findEnvironmentsByProjectId>
|
|
>[number];
|
|
interface AdvancedEnvironmentSelectorProps {
|
|
projectId: string;
|
|
currentEnvironmentId?: string;
|
|
}
|
|
|
|
export const AdvancedEnvironmentSelector = ({
|
|
projectId,
|
|
currentEnvironmentId,
|
|
}: AdvancedEnvironmentSelectorProps) => {
|
|
const router = useRouter();
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [selectedEnvironment, setSelectedEnvironment] =
|
|
useState<Environment | null>(null);
|
|
|
|
const { data: environments } = api.environment.byProjectId.useQuery(
|
|
{ projectId: projectId },
|
|
{
|
|
enabled: !!projectId,
|
|
},
|
|
);
|
|
|
|
// Form states
|
|
const [name, setName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
|
|
// Get current user's permissions
|
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
|
|
// Check if user can create environments
|
|
const canCreateEnvironments = !!permissions?.environment.create;
|
|
|
|
// Check if user can delete environments
|
|
const canDeleteEnvironments = !!permissions?.environment.delete;
|
|
|
|
const haveServices =
|
|
selectedEnvironment &&
|
|
((selectedEnvironment?.mariadb?.length || 0) > 0 ||
|
|
(selectedEnvironment?.mongo?.length || 0) > 0 ||
|
|
(selectedEnvironment?.mysql?.length || 0) > 0 ||
|
|
(selectedEnvironment?.postgres?.length || 0) > 0 ||
|
|
(selectedEnvironment?.redis?.length || 0) > 0 ||
|
|
(selectedEnvironment?.applications?.length || 0) > 0 ||
|
|
(selectedEnvironment?.compose?.length || 0) > 0);
|
|
const createEnvironment = api.environment.create.useMutation();
|
|
const updateEnvironment = api.environment.update.useMutation();
|
|
const deleteEnvironment = api.environment.remove.useMutation();
|
|
const duplicateEnvironment = api.environment.duplicate.useMutation();
|
|
|
|
// Refetch project data
|
|
const utils = api.useUtils();
|
|
|
|
const handleCreateEnvironment = async () => {
|
|
try {
|
|
await createEnvironment.mutateAsync({
|
|
projectId,
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
});
|
|
|
|
toast.success("Environment created successfully");
|
|
utils.environment.byProjectId.invalidate({ projectId });
|
|
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
|
utils.project.all.invalidate();
|
|
setIsCreateDialogOpen(false);
|
|
setName("");
|
|
setDescription("");
|
|
} catch (error) {
|
|
toast.error(
|
|
`Failed to create environment: ${error instanceof Error ? error.message : error}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleUpdateEnvironment = async () => {
|
|
if (!selectedEnvironment) return;
|
|
|
|
try {
|
|
await updateEnvironment.mutateAsync({
|
|
environmentId: selectedEnvironment.environmentId,
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
});
|
|
|
|
toast.success("Environment updated successfully");
|
|
utils.environment.byProjectId.invalidate({ projectId });
|
|
setIsEditDialogOpen(false);
|
|
setSelectedEnvironment(null);
|
|
setName("");
|
|
setDescription("");
|
|
} catch (error) {
|
|
toast.error(
|
|
`Failed to update environment: ${error instanceof Error ? error.message : error}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleDeleteEnvironment = async () => {
|
|
if (!selectedEnvironment) return;
|
|
|
|
try {
|
|
await deleteEnvironment.mutateAsync({
|
|
environmentId: selectedEnvironment.environmentId,
|
|
});
|
|
|
|
toast.success("Environment deleted successfully");
|
|
utils.environment.byProjectId.invalidate({ projectId });
|
|
setIsDeleteDialogOpen(false);
|
|
setSelectedEnvironment(null);
|
|
|
|
// Redirect to first available environment if we deleted the current environment
|
|
if (selectedEnvironment.environmentId === currentEnvironmentId) {
|
|
const firstEnv = environments?.find(
|
|
(env) => env.environmentId !== selectedEnvironment.environmentId,
|
|
);
|
|
if (firstEnv) {
|
|
router.push(
|
|
`/dashboard/project/${projectId}/environment/${firstEnv.environmentId}`,
|
|
);
|
|
} else {
|
|
// No other environments, redirect to project page
|
|
router.push(`/dashboard/project/${projectId}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
toast.error("Failed to delete environment");
|
|
}
|
|
};
|
|
|
|
const handleDuplicateEnvironment = async (environment: Environment) => {
|
|
try {
|
|
const result = await duplicateEnvironment.mutateAsync({
|
|
environmentId: environment.environmentId,
|
|
name: `${environment.name}-copy`,
|
|
description: environment.description || undefined,
|
|
});
|
|
|
|
toast.success("Environment duplicated successfully");
|
|
utils.project.one.invalidate({ projectId });
|
|
|
|
// Navigate to the new duplicated environment
|
|
router.push(
|
|
`/dashboard/project/${projectId}/environment/${result.environmentId}`,
|
|
);
|
|
} catch (error) {
|
|
toast.error("Failed to duplicate environment");
|
|
}
|
|
};
|
|
|
|
const openEditDialog = (environment: Environment) => {
|
|
setSelectedEnvironment(environment);
|
|
setName(environment.name);
|
|
setDescription(environment.description || "");
|
|
setIsEditDialogOpen(true);
|
|
};
|
|
|
|
const openDeleteDialog = (environment: Environment) => {
|
|
setSelectedEnvironment(environment);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
const currentEnv = environments?.find(
|
|
(env) => env.environmentId === currentEnvironmentId,
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-auto p-2 font-normal">
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-muted-foreground">/</span>
|
|
<span>{currentEnv?.name || "Select Environment"}</span>
|
|
<ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-[300px]" align="start">
|
|
<DropdownMenuLabel>Environments</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
|
|
{environments?.map((environment) => {
|
|
const servicesCount =
|
|
environment.mariadb.length +
|
|
environment.mongo.length +
|
|
environment.mysql.length +
|
|
environment.postgres.length +
|
|
environment.redis.length +
|
|
environment.applications.length +
|
|
environment.compose.length;
|
|
return (
|
|
<div
|
|
key={environment.environmentId}
|
|
className="flex items-center"
|
|
>
|
|
<DropdownMenuItem
|
|
className="flex-1 cursor-pointer"
|
|
onClick={() => {
|
|
router.push(
|
|
`/dashboard/project/${projectId}/environment/${environment.environmentId}`,
|
|
);
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>
|
|
{environment.name} ({servicesCount})
|
|
</span>
|
|
{environment.environmentId === currentEnvironmentId && (
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
|
)}
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<div className="flex items-center gap-1 px-2">
|
|
{!environment.isDefault && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openEditDialog(environment);
|
|
}}
|
|
>
|
|
<PencilIcon className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
{canDeleteEnvironments && !environment.isDefault && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openDeleteDialog(environment);
|
|
}}
|
|
>
|
|
<TrashIcon className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
<DropdownMenuSeparator />
|
|
{canCreateEnvironments && (
|
|
<DropdownMenuItem
|
|
className="cursor-pointer"
|
|
onClick={() => setIsCreateDialogOpen(true)}
|
|
>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Create Environment
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create Environment</DialogTitle>
|
|
<DialogDescription>
|
|
Create a new environment for your project.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Environment name"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Description (optional)</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Environment description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setIsCreateDialogOpen(false);
|
|
setName("");
|
|
setDescription("");
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleCreateEnvironment}
|
|
disabled={!name.trim() || createEnvironment.isPending}
|
|
>
|
|
{createEnvironment.isPending ? "Creating..." : "Create"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Environment Dialog */}
|
|
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Environment</DialogTitle>
|
|
<DialogDescription>
|
|
Update the environment details.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="edit-name">Name</Label>
|
|
<Input
|
|
id="edit-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Environment name"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="edit-description">Description (optional)</Label>
|
|
<Textarea
|
|
id="edit-description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Environment description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setIsEditDialogOpen(false);
|
|
setSelectedEnvironment(null);
|
|
setName("");
|
|
setDescription("");
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleUpdateEnvironment}
|
|
disabled={!name.trim() || updateEnvironment.isPending}
|
|
>
|
|
{updateEnvironment.isPending ? "Updating..." : "Update"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Environment Dialog */}
|
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Environment</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete the environment "
|
|
{selectedEnvironment?.name}"? This action cannot be undone and
|
|
will also delete all services in this environment.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{haveServices && (
|
|
<AlertBlock type="warning">
|
|
This environment have active services, please delete them first.
|
|
</AlertBlock>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setIsDeleteDialogOpen(false);
|
|
setSelectedEnvironment(null);
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDeleteEnvironment}
|
|
disabled={
|
|
deleteEnvironment.isPending ||
|
|
haveServices ||
|
|
!selectedEnvironment
|
|
}
|
|
>
|
|
{deleteEnvironment.isPending ? "Deleting..." : "Delete"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|