diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index ca83b588..8d058b6c 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -1,5 +1,6 @@ import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; +import { StatusTooltip } from "@/components/shared/status-tooltip"; import { AlertDialog, AlertDialogAction, @@ -176,8 +177,11 @@ export const ShowProjects = () => {
- + {app.name} + {app.domains.map((domain) => ( @@ -209,8 +213,11 @@ export const ShowProjects = () => {
- + {comp.name} + {comp.domains.map((domain) => ( diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index f9fc2d49..26317ca9 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -13,6 +13,7 @@ import { import { ProjectLayout } from "@/components/layouts/project-layout"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; +import { DialogAction } from "@/components/shared/dialog-action"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Button } from "@/components/ui/button"; import { @@ -23,6 +24,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; import { Command, CommandEmpty, @@ -50,22 +52,27 @@ import type { findProjectById } from "@dokploy/server"; import { validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import { + Ban, + Check, + CheckCircle2, + ChevronsUpDown, CircuitBoard, FolderInput, GlobeIcon, Loader2, PlusIcon, Search, + X, } from "lucide-react"; -import { Check, ChevronsUpDown, X } from "lucide-react"; import type { GetServerSidePropsContext, InferGetServerSidePropsType, } from "next"; import Head from "next/head"; import { useRouter } from "next/router"; -import React, { useMemo, useState, type ReactElement } from "react"; +import { useMemo, useState, type ReactElement } from "react"; import superjson from "superjson"; +import { toast } from "sonner"; export type Services = { appName: string; @@ -191,6 +198,7 @@ export const extractServices = (data: Project | undefined) => { const Project = ( props: InferGetServerSidePropsType, ) => { + const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const { projectId } = props; const { data: auth } = api.auth.get.useQuery(); const { data: user } = api.user.byAuthId.useQuery( @@ -201,7 +209,7 @@ const Project = ( enabled: !!auth?.id && auth?.rol === "user", }, ); - const { data, isLoading } = api.project.one.useQuery({ projectId }); + const { data, isLoading, refetch } = api.project.one.useQuery({ projectId }); const router = useRouter(); const emptyServices = @@ -228,6 +236,70 @@ const Project = ( const [selectedTypes, setSelectedTypes] = useState([]); const [openCombobox, setOpenCombobox] = useState(false); + const [selectedServices, setSelectedServices] = useState([]); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const handleSelectAll = () => { + if (selectedServices.length === filteredServices.length) { + setSelectedServices([]); + } else { + setSelectedServices(filteredServices.map((service) => service.id)); + } + }; + + const handleServiceSelect = (serviceId: string, event: React.MouseEvent) => { + event.stopPropagation(); + setSelectedServices((prev) => + prev.includes(serviceId) + ? prev.filter((id) => id !== serviceId) + : [...prev, serviceId], + ); + }; + + const composeActions = { + start: api.compose.start.useMutation(), + stop: api.compose.stop.useMutation(), + }; + + const handleBulkStart = async () => { + let success = 0; + setIsBulkActionLoading(true); + for (const serviceId of selectedServices) { + try { + await composeActions.start.mutateAsync({ composeId: serviceId }); + success++; + } catch (error) { + toast.error(`Error starting service ${serviceId}`); + } + } + if (success > 0) { + toast.success(`${success} services started successfully`); + refetch(); + } + setIsBulkActionLoading(false); + setSelectedServices([]); + setIsDropdownOpen(false); + }; + + const handleBulkStop = async () => { + let success = 0; + setIsBulkActionLoading(true); + for (const serviceId of selectedServices) { + try { + await composeActions.stop.mutateAsync({ composeId: serviceId }); + success++; + } catch (error) { + toast.error(`Error stopping service ${serviceId}`); + } + } + if (success > 0) { + toast.success(`${success} services stopped successfully`); + refetch(); + } + setSelectedServices([]); + setIsDropdownOpen(false); + setIsBulkActionLoading(false); + }; const filteredServices = useMemo(() => { if (!applications) return []; @@ -309,78 +381,151 @@ const Project = (
) : ( <> -
-
- setSearchQuery(e.target.value)} - className="pr-10" - /> - +
+
+
+ 0} + className={cn( + "data-[state=checked]:bg-primary", + selectedServices.length > 0 && + selectedServices.length < + filteredServices.length && + "bg-primary/50", + )} + onCheckedChange={handleSelectAll} + /> + + Select All{" "} + {selectedServices.length > 0 && + `(${selectedServices.length}/${filteredServices.length})`} + +
+ + + + + + + Actions + + + + + + + + +
- - - - - - - - No type found. - - {serviceTypes.map((type) => ( + +
+
+ setSearchQuery(e.target.value)} + className="pr-10" + /> + +
+ + + + + + + + No type found. + + {serviceTypes.map((type) => ( + { + setSelectedTypes((prev) => + prev.includes(type.value) + ? prev.filter((t) => t !== type.value) + : [...prev, type.value], + ); + setOpenCombobox(false); + }} + > +
+ + {type.icon && ( + + )} + {type.label} +
+
+ ))} { - setSelectedTypes((prev) => - prev.includes(type.value) - ? prev.filter((t) => t !== type.value) - : [...prev, type.value], - ); + setSelectedTypes([]); setOpenCombobox(false); }} + className="border-t" > -
- - {type.icon && ( - - )} - {type.label} +
+ + Clear filters
- ))} - { - setSelectedTypes([]); - setOpenCombobox(false); - }} - className="border-t" - > -
- - Clear filters -
-
- - - - + + + + +
@@ -418,6 +563,27 @@ const Project = (
+
+ handleServiceSelect(service.id, e) + } + > +
+ +
+
+