import { AlertTriangle, ArrowUpDown, BookIcon, ExternalLinkIcon, FolderInput, Loader2, MoreHorizontalIcon, Search, TrashIcon, } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { TimeBadge } from "@/components/ui/time-badge"; import { api } from "@/utils/api"; import { useDebounce } from "@/utils/hooks/use-debounce"; import { HandleProject } from "./handle-project"; import { ProjectEnvironment } from "./project-environment"; export const ShowProjects = () => { const utils = api.useUtils(); const router = useRouter(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data, isLoading } = api.project.all.useQuery(); const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); const [searchQuery, setSearchQuery] = useState( router.isReady && typeof router.query.q === "string" ? router.query.q : "", ); const debouncedSearchQuery = useDebounce(searchQuery, 500); const [sortBy, setSortBy] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("projectsSort") || "createdAt-desc"; } return "createdAt-desc"; }); useEffect(() => { localStorage.setItem("projectsSort", sortBy); }, [sortBy]); useEffect(() => { if (!router.isReady) return; const urlQuery = typeof router.query.q === "string" ? router.query.q : ""; if (urlQuery !== searchQuery) { setSearchQuery(urlQuery); } }, [router.isReady, router.query.q]); useEffect(() => { if (!router.isReady) return; const urlQuery = typeof router.query.q === "string" ? router.query.q : ""; if (debouncedSearchQuery === urlQuery) return; const newQuery = { ...router.query }; if (debouncedSearchQuery) { newQuery.q = debouncedSearchQuery; } else { delete newQuery.q; } router.replace({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true, }); }, [debouncedSearchQuery]); const filteredProjects = useMemo(() => { if (!data) return []; const filtered = data.filter( (project) => project.name .toLowerCase() .includes(debouncedSearchQuery.toLowerCase()) || project.description ?.toLowerCase() .includes(debouncedSearchQuery.toLowerCase()), ); // Then sort the filtered results const [field, direction] = sortBy.split("-"); return [...filtered].sort((a, b) => { let comparison = 0; switch (field) { case "name": comparison = a.name.localeCompare(b.name); break; case "createdAt": comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); break; case "services": { const aTotalServices = a.environments.reduce((total, env) => { return ( total + (env.applications?.length || 0) + (env.mariadb?.length || 0) + (env.mongo?.length || 0) + (env.mysql?.length || 0) + (env.postgres?.length || 0) + (env.redis?.length || 0) + (env.compose?.length || 0) ); }, 0); const bTotalServices = b.environments.reduce((total, env) => { return ( total + (env.applications?.length || 0) + (env.mariadb?.length || 0) + (env.mongo?.length || 0) + (env.mysql?.length || 0) + (env.postgres?.length || 0) + (env.redis?.length || 0) + (env.compose?.length || 0) ); }, 0); comparison = aTotalServices - bTotalServices; break; } default: comparison = 0; } return direction === "asc" ? comparison : -comparison; }); }, [data, debouncedSearchQuery, sortBy]); return ( <> {!isCloud && (
)}
Projects Create and manage your projects {(auth?.role === "owner" || auth?.role === "admin" || auth?.canCreateProjects) && (
)}
{isLoading ? (
Loading...
) : ( <>
setSearchQuery(e.target.value)} className="pr-10" />
{filteredProjects?.length === 0 && (
No projects found
)}
{filteredProjects?.map((project) => { const emptyServices = project?.environments .map( (env) => env.applications.length === 0 && env.mariadb.length === 0 && env.mongo.length === 0 && env.mysql.length === 0 && env.postgres.length === 0 && env.redis.length === 0 && env.applications.length === 0 && env.compose.length === 0, ) .every(Boolean); const totalServices = project?.environments .map( (env) => env.mariadb.length + env.mongo.length + env.mysql.length + env.postgres.length + env.redis.length + env.applications.length + env.compose.length, ) .reduce((acc, curr) => acc + curr, 0); const haveServicesWithDomains = project?.environments .map( (env) => env.applications.length > 0 || env.compose.length > 0, ) .some(Boolean); const productionEnvironment = project?.environments.find( (env) => env.isDefault, ); return (
{haveServicesWithDomains ? ( e.stopPropagation()} > {project.environments.some( (env) => env.applications.length > 0, ) && ( Applications {project.environments.map((env) => env.applications.map((app) => (
{app.name} {app.domains.map((domain) => ( {domain.host} ))}
)), )}
)} {project.environments.some( (env) => env.compose.length > 0, ) && ( Compose {project.environments.map((env) => env.compose.map((comp) => (
{comp.name} {comp.domains.map((domain) => ( {domain.host} ))}
)), )}
)}
) : null}
{project.name}
{project.description}
e.stopPropagation()} > Actions
e.stopPropagation()} >
e.stopPropagation()} >
e.stopPropagation()} > {(auth?.role === "owner" || auth?.canDeleteProjects) && ( e.preventDefault() } > Delete Are you sure to delete this project? {!emptyServices ? (
You have active services, please delete them first
) : ( This action cannot be undone )}
Cancel { await mutateAsync({ projectId: project.projectId, }) .then(() => { toast.success( "Project deleted successfully", ); }) .catch(() => { toast.error( "Error deleting this project", ); }) .finally(() => { utils.project.all.invalidate(); }); }} > Delete
)}
Created {totalServices}{" "} {totalServices === 1 ? "service" : "services"}
); })}
)}
); };