+
{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)
+ }
+ >
+
+
+
+
+