Merge pull request #3857 from Dokploy/feat/improve-queries

feat: enhance project and environment services with additional column…
This commit is contained in:
Mauricio Siu
2026-03-01 14:24:16 -06:00
committed by GitHub
7 changed files with 306 additions and 162 deletions

View File

@@ -25,7 +25,6 @@ import {
import { api } from "@/utils/api";
export type Services = {
appName: string;
serverId?: string | null;
name: string;
type:

View File

@@ -2,7 +2,6 @@ import {
AlertTriangle,
ArrowUpDown,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
@@ -16,7 +15,6 @@ 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,
@@ -40,10 +38,8 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -280,14 +276,6 @@ export const ShowProjects = () => {
)
.reduce((acc, curr) => acc + curr, 0);
const haveServicesWithDomains = project?.environments
.map(
(env) =>
env.applications.length > 0 ||
env.compose.length > 0,
)
.some(Boolean);
// Find default environment from accessible environments, or fall back to first accessible environment
const accessibleEnvironment =
project?.environments.find((env) => env.isDefault) ||
@@ -313,122 +301,6 @@ export const ShowProjects = () => {
}}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
{haveServicesWithDomains ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.environments.some(
(env) => env.applications.length > 0,
) && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Applications
</DropdownMenuLabel>
{project.environments.map((env) =>
env.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{app.name}
<StatusTooltip
status={
app.applicationStatus
}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup>
)}
{project.environments.some(
(env) => env.compose.length > 0,
) && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Compose
</DropdownMenuLabel>
{project.environments.map((env) =>
env.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.name}
<StatusTooltip
status={comp.composeStatus}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 ">

View File

@@ -100,7 +100,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
const url = useUrl();
const { data: projects } = api.project.all.useQuery();
const { data: projects } = api.project.allForPermissions.useQuery();
const extractServicesFromProjects = () => {
if (!projects) return [];

View File

@@ -28,8 +28,12 @@ import {
import { Switch } from "@/components/ui/switch";
import { api, type RouterOutputs } from "@/utils/api";
type Project = RouterOutputs["project"]["all"][number];
type Environment = Project["environments"][number];
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
type ProjectForPermissions =
RouterOutputs["project"]["allForPermissions"][number];
type EnvironmentForPermissions = ProjectForPermissions["environments"][number];
type Environment = EnvironmentForPermissions;
export type Services = {
appName: string;
@@ -173,7 +177,9 @@ interface Props {
export const AddUserPermissions = ({ userId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: projects } = api.project.all.useQuery();
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
enabled: isOpen,
});
const { data, refetch } = api.user.one.useQuery(
{

View File

@@ -100,7 +100,6 @@ import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
export type Services = {
appName: string;
serverId?: string | null;
serverName?: string | null;
name: string;
@@ -146,7 +145,6 @@ export const extractServicesFromEnvironment = (
}
}
return {
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
@@ -161,7 +159,6 @@ export const extractServicesFromEnvironment = (
const mariadb: Services[] =
environment.mariadb?.map((item) => ({
appName: item.appName,
name: item.name,
type: "mariadb",
id: item.mariadbId,
@@ -174,7 +171,6 @@ export const extractServicesFromEnvironment = (
const postgres: Services[] =
environment.postgres?.map((item) => ({
appName: item.appName,
name: item.name,
type: "postgres",
id: item.postgresId,
@@ -187,7 +183,6 @@ export const extractServicesFromEnvironment = (
const mongo: Services[] =
environment.mongo?.map((item) => ({
appName: item.appName,
name: item.name,
type: "mongo",
id: item.mongoId,
@@ -200,7 +195,6 @@ export const extractServicesFromEnvironment = (
const redis: Services[] =
environment.redis?.map((item) => ({
appName: item.appName,
name: item.name,
type: "redis",
id: item.redisId,
@@ -213,7 +207,6 @@ export const extractServicesFromEnvironment = (
const mysql: Services[] =
environment.mysql?.map((item) => ({
appName: item.appName,
name: item.name,
type: "mysql",
id: item.mysqlId,
@@ -242,7 +235,6 @@ export const extractServicesFromEnvironment = (
}
}
return {
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,

View File

@@ -37,7 +37,11 @@ import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import {
apiCreateProject,
apiFindOneProject,
@@ -219,31 +223,69 @@ export const projectRouter = createTRPCRouter({
applications.applicationId,
accessedServices,
),
with: { domains: true },
columns: {
applicationId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
columns: {
mariadbId: true,
name: true,
applicationStatus: true,
},
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
columns: {
mongoId: true,
name: true,
applicationStatus: true,
},
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
columns: {
mysqlId: true,
name: true,
applicationStatus: true,
},
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
columns: {
postgresId: true,
name: true,
applicationStatus: true,
},
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
columns: {
redisId: true,
name: true,
applicationStatus: true,
},
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
columns: {
composeId: true,
name: true,
composeStatus: true,
},
},
},
columns: {
environmentId: true,
isDefault: true,
name: true,
},
},
},
orderBy: desc(projects.createdAt),
@@ -255,21 +297,50 @@ export const projectRouter = createTRPCRouter({
environments: {
with: {
applications: {
with: {
domains: true,
columns: {
applicationId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
columns: {
mariadbId: true,
},
},
mongo: {
columns: {
mongoId: true,
},
},
mysql: {
columns: {
mysqlId: true,
},
},
postgres: {
columns: {
postgresId: true,
},
},
redis: {
columns: {
redisId: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
columns: {
composeId: true,
name: true,
composeStatus: true,
},
},
},
columns: {
name: true,
environmentId: true,
isDefault: true,
},
},
},
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
@@ -277,6 +348,106 @@ export const projectRouter = createTRPCRouter({
});
}),
/** All projects with full environments and services for the admin permissions UI. Admin only. */
allForPermissions: adminProcedure.query(async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
},
with: {
environments: {
columns: {
environmentId: true,
name: true,
isDefault: true,
},
with: {
applications: {
columns: {
applicationId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mariadb: {
columns: {
mariadbId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
postgres: {
columns: {
postgresId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mysql: {
columns: {
mysqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mongo: {
columns: {
mongoId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
redis: {
columns: {
redisId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
},
},
},
});
}),
search: protectedProcedure
.input(
z.object({

View File

@@ -34,42 +34,139 @@ export const createEnvironment = async (
export const findEnvironmentById = async (environmentId: string) => {
const environment = await db.query.environments.findFirst({
where: eq(environments.environmentId, environmentId),
columns: {
name: true,
description: true,
environmentId: true,
isDefault: true,
projectId: true,
env: true,
},
with: {
applications: {
with: {
deployments: true,
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
name: true,
applicationId: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mariadb: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
mariadbId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mongo: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
mongoId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
mysql: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
mysqlId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
postgres: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
postgresId: true,
name: true,
description: true,
createdAt: true,
applicationStatus: true,
serverId: true,
},
},
redis: {
with: {
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
redisId: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
compose: {
with: {
deployments: true,
server: true,
server: {
columns: {
name: true,
serverId: true,
},
},
},
columns: {
composeId: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
project: true,
@@ -98,6 +195,12 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
compose: true,
project: true,
},
columns: {
name: true,
description: true,
environmentId: true,
isDefault: true,
},
});
return projectEnvironments;
};
@@ -169,6 +272,7 @@ export const duplicateEnvironment = async (
name: input.name,
description: input.description || originalEnvironment.description,
projectId: originalEnvironment.projectId,
env: originalEnvironment.env,
})
.returning()
.then((value) => value[0]);