Merge pull request #4261 from Dokploy/canary

🚀 Release v0.29.1
This commit is contained in:
Mauricio Siu
2026-04-20 07:16:11 -06:00
committed by GitHub
37 changed files with 584 additions and 62 deletions
+21 -20
View File
@@ -3,6 +3,7 @@ name: Sync version to MCP and CLI repos
on:
release:
types: [published]
workflow_dispatch:
jobs:
sync-version:
@@ -15,55 +16,55 @@ jobs:
- name: Get version
id: get_version
run: |
VERSION=$(jq -r .version apps/dokploy/package.json)
VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Sync version to MCP repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
cd mcp-repo
# Bump version
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
cd /tmp/mcp-repo
# Regenerate tools from latest OpenAPI spec
npm install -g pnpm
pnpm install
pnpm run fetch-openapi
pnpm run generate
# Bump version after install so pnpm install doesn't overwrite it
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Release: ${{ github.event.release.html_url }}" \
--allow-empty
git push
- name: Sync version to CLI repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
cd cli-repo
cd /tmp/cli-repo
# Bump version
# Copy latest openapi spec and regenerate commands
cp ${{ github.workspace }}/openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
# Bump version after install so pnpm install doesn't overwrite it
if [ -f package.json ]; then
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
fi
# Copy latest openapi spec and regenerate commands
cp ../openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<div className="space-y-0.5">
<FormLabel>Custom Entrypoint</FormLabel>
<FormDescription>
Use custom entrypoint for domina
Use custom entrypoint for domain
<br />
"web" and/or "websecure" is used by default.
</FormDescription>
@@ -0,0 +1,291 @@
import { formatDistanceToNow } from "date-fns";
import { ArrowRight, Rocket, Server } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
type DeploymentStatus = "idle" | "running" | "done" | "error";
const statusDotClass: Record<string, string> = {
done: "bg-emerald-500",
running: "bg-amber-500",
error: "bg-red-500",
idle: "bg-muted-foreground/40",
};
function getServiceInfo(d: any) {
const app = d.application;
const comp = d.compose;
const serverName: string =
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
if (app?.environment?.project && app.environment) {
return {
name: app.name as string,
environment: app.environment.name as string,
projectName: app.environment.project.name as string,
serverName,
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
};
}
if (comp?.environment?.project && comp.environment) {
return {
name: comp.name as string,
environment: comp.environment.name as string,
projectName: comp.environment.project.name as string,
serverName,
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
};
}
return null;
}
function StatCard({
label,
value,
delta,
}: {
label: string;
value: string;
delta?: string;
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<div className="flex flex-col gap-1">
<span className="text-3xl font-semibold tracking-tight">{value}</span>
{delta && (
<span className="text-xs text-muted-foreground">{delta}</span>
)}
</div>
</div>
);
}
function StatusListCard({
label,
items,
}: {
label: string;
items: { dotClass: string; label: string; count: number }[];
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<ul className="flex flex-col gap-1.5">
{items.map((item) => (
<li key={item.label} className="flex items-center gap-2.5 text-sm">
<span
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
aria-hidden
/>
<span className="font-semibold tabular-nums w-8">{item.count}</span>
<span className="text-muted-foreground">{item.label}</span>
</li>
))}
</ul>
</div>
);
}
export const ShowHome = () => {
const { data: auth } = api.user.get.useQuery();
const { data: homeStats } = api.project.homeStats.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const canReadDeployments = !!permissions?.deployment.read;
const { data: deployments } = api.deployment.allCentralized.useQuery(
undefined,
{
enabled: canReadDeployments,
refetchInterval: 10000,
},
);
const firstName = auth?.user?.firstName?.trim();
const totals = homeStats ?? {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
};
const statusBreakdown = homeStats?.status ?? {
running: 0,
error: 0,
idle: 0,
};
const recentDeployments = useMemo(() => {
if (!deployments) return [];
return [...deployments]
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 10);
}, [deployments]);
const deployStats = useMemo(() => {
const now = Date.now();
const weekMs = 7 * 24 * 60 * 60 * 1000;
const lastStart = now - weekMs;
const prevStart = now - 2 * weekMs;
const last: NonNullable<typeof deployments> = [];
const prev: NonNullable<typeof deployments> = [];
for (const d of deployments ?? []) {
const t = new Date(d.createdAt).getTime();
if (t >= lastStart) last.push(d);
else if (t >= prevStart) prev.push(d);
}
const lastCount = last.length;
const prevCount = prev.length;
let delta: string | undefined;
if (prevCount > 0) {
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
} else if (lastCount > 0) {
delta = "no prior data";
} else {
delta = "no activity yet";
}
return { value: String(lastCount), delta };
}, [deployments]);
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<h1 className="text-3xl font-semibold tracking-tight">
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
</h1>
<Button asChild variant="secondary" className="w-fit">
<Link href="/dashboard/projects">
Go to projects
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Projects"
value={String(totals.projects)}
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
/>
<StatCard
label="Services"
value={String(totals.services)}
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
/>
<StatCard
label="Deploys / 7d"
value={deployStats.value}
delta={deployStats.delta}
/>
<StatusListCard
label="Status"
items={[
{
dotClass: "bg-emerald-500",
label: "running",
count: statusBreakdown.running,
},
{
dotClass: "bg-red-500",
label: "errored",
count: statusBreakdown.error,
},
{
dotClass: "bg-muted-foreground/40",
label: "idle",
count: statusBreakdown.idle,
},
]}
/>
</div>
<div className="rounded-xl border bg-background">
<div className="flex items-center justify-between px-5 py-4 border-b">
<div className="flex items-center gap-2">
<Rocket className="size-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Recent deployments</h2>
</div>
{canReadDeployments && (
<Link
href="/dashboard/deployments"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
view all
</Link>
)}
</div>
{!canReadDeployments ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>You do not have permission to view deployments.</span>
</div>
) : recentDeployments.length === 0 ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>No deployments yet.</span>
</div>
) : (
<ul className="divide-y">
{recentDeployments.map((d) => {
const info = getServiceInfo(d);
if (!info) return null;
const status = (d.status ?? "idle") as DeploymentStatus;
return (
<li key={d.deploymentId}>
<Link
href={info.href}
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
>
<span
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
aria-hidden
/>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-sm truncate">{info.name}</span>
<span className="text-xs text-muted-foreground truncate">
{info.projectName} · {info.environment}
</span>
</div>
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
<Server className="size-3 shrink-0" />
<span className="truncate">{info.serverName}</span>
</span>
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
{status}
</span>
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
{formatDistanceToNow(new Date(d.createdAt), {
addSuffix: true,
})}
</span>
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
logs
</span>
</Link>
</li>
);
})}
</ul>
)}
</div>
</div>
</Card>
</div>
);
};
@@ -166,6 +166,7 @@ export const ShowProjects = () => {
return (
total +
(env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
@@ -178,6 +179,7 @@ export const ShowProjects = () => {
return (
total +
(env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
@@ -167,7 +167,7 @@ export const SearchCommand = () => {
<CommandGroup heading={"Application"} hidden={true}>
<CommandItem
onSelect={() => {
router.push("/dashboard/projects");
router.push("/dashboard/home");
setOpen(false);
}}
>
@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
onClick={() => {
if (stepper.isLast) {
setIsOpen(false);
push("/dashboard/projects");
push("/dashboard/home");
} else {
stepper.next();
}
+7
View File
@@ -19,6 +19,7 @@ import {
Forward,
GalleryVerticalEnd,
GitBranch,
House,
Key,
KeyRound,
Loader2,
@@ -148,6 +149,12 @@ type Menu = {
// The `isEnabled` function is called to determine if the item should be displayed
const MENU: Menu = {
home: [
{
isSingle: true,
title: "Home",
url: "/dashboard/home",
icon: House,
},
{
isSingle: true,
title: "Projects",
+1 -1
View File
@@ -80,7 +80,7 @@ export const UserNav = () => {
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/projects");
router.push("/dashboard/home");
}}
>
Projects
@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
try {
const { data, error } = await authClient.signIn.sso({
email: values.email,
callbackURL: "/dashboard/projects",
callbackURL: "/dashboard/home",
});
if (error) {
toast.error(error.message ?? "Failed to sign in with SSO");
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.29.0",
"version": "v0.29.1",
"private": true,
"license": "Apache-2.0",
"type": "module",
+1 -1
View File
@@ -53,7 +53,7 @@ export default function Custom404({ statusCode, error }: Props) {
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
<Link
href="/dashboard/projects"
href="/dashboard/home"
className={buttonVariants({
variant: "secondary",
className: "flex flex-row gap-2",
+1 -1
View File
@@ -102,7 +102,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+1 -1
View File
@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+53
View File
@@ -0,0 +1,53 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowHome } from "@/components/dashboard/home/show-home";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
const Home = () => {
return <ShowHome />;
};
export default Home;
Home.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}
+2 -2
View File
@@ -96,7 +96,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -122,7 +122,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -509,6 +509,14 @@ const EnvironmentPage = (
deploy: api.mongo.deploy.useMutation(),
};
const libsqlActions = {
start: api.libsql.start.useMutation(),
stop: api.libsql.stop.useMutation(),
move: api.libsql.move.useMutation(),
delete: api.libsql.remove.useMutation(),
deploy: api.libsql.deploy.useMutation(),
};
const handleBulkStart = async () => {
let success = 0;
setIsBulkActionLoading(true);
@@ -541,6 +549,9 @@ const EnvironmentPage = (
case "mongo":
await mongoActions.start.mutateAsync({ mongoId: serviceId });
break;
case "libsql":
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
break;
}
success++;
} catch {
@@ -588,6 +599,9 @@ const EnvironmentPage = (
case "mongo":
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
break;
case "libsql":
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
break;
}
success++;
} catch {
@@ -664,6 +678,12 @@ const EnvironmentPage = (
targetEnvironmentId: selectedTargetEnvironment,
});
break;
case "libsql":
await libsqlActions.move.mutateAsync({
libsqlId: serviceId,
targetEnvironmentId: selectedTargetEnvironment,
});
break;
}
await utils.environment.one.invalidate({
environmentId,
@@ -733,6 +753,11 @@ const EnvironmentPage = (
mongoId: serviceId,
});
break;
case "libsql":
await libsqlActions.delete.mutateAsync({
libsqlId: serviceId,
});
break;
}
await utils.environment.one.invalidate({
environmentId,
@@ -799,6 +824,11 @@ const EnvironmentPage = (
mongoId: serviceId,
});
break;
case "libsql":
await libsqlActions.deploy.mutateAsync({
libsqlId: serviceId,
});
break;
}
success++;
} catch (error) {
@@ -1856,7 +1886,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -490,7 +490,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -492,7 +492,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -343,7 +343,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -372,7 +372,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -376,7 +376,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -353,7 +353,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -360,7 +360,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -363,7 +363,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+1 -1
View File
@@ -18,7 +18,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+1 -1
View File
@@ -35,7 +35,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -28,7 +28,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -45,7 +45,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+1 -1
View File
@@ -46,7 +46,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+1 -1
View File
@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+5 -5
View File
@@ -106,7 +106,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while logging in");
} finally {
@@ -133,7 +133,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while verifying 2FA code");
} finally {
@@ -163,7 +163,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while verifying backup code");
} finally {
@@ -408,7 +408,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -437,7 +437,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+1 -1
View File
@@ -139,7 +139,7 @@ const Invitation = ({
});
toast.success("Account created successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while creating your account");
}
+1 -1
View File
@@ -303,7 +303,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
+142
View File
@@ -487,6 +487,148 @@ export const projectRouter = createTRPCRouter({
},
),
homeStats: protectedProcedure.query(async ({ ctx }) => {
const isPrivileged = ctx.user.role === "owner" || ctx.user.role === "admin";
let accessedProjects: string[] = [];
let accessedEnvironments: string[] = [];
let accessedServices: string[] = [];
if (!isPrivileged) {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
accessedProjects = member.accessedProjects;
accessedEnvironments = member.accessedEnvironments;
accessedServices = member.accessedServices;
if (accessedProjects.length === 0) {
return {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
status: { running: 0, error: 0, idle: 0 },
};
}
}
const projectIdFilter = isPrivileged
? eq(projects.organizationId, ctx.session.activeOrganizationId)
: and(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((id) => sql`${id}`),
sql`, `,
)})`,
eq(projects.organizationId, ctx.session.activeOrganizationId),
);
const environmentFilter = isPrivileged
? undefined
: accessedEnvironments.length === 0
? sql`false`
: sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((envId) => sql`${envId}`),
sql`, `,
)})`;
const applyFilter = (col: AnyPgColumn) =>
isPrivileged ? undefined : buildServiceFilter(col, accessedServices);
const rows = await db.query.projects.findMany({
where: projectIdFilter,
columns: { projectId: true },
with: {
environments: {
where: environmentFilter,
columns: { environmentId: true },
with: {
applications: {
where: applyFilter(applications.applicationId),
columns: { applicationStatus: true },
},
compose: {
where: applyFilter(compose.composeId),
columns: { composeStatus: true },
},
libsql: {
where: applyFilter(libsql.libsqlId),
columns: { applicationStatus: true },
},
mariadb: {
where: applyFilter(mariadb.mariadbId),
columns: { applicationStatus: true },
},
mongo: {
where: applyFilter(mongo.mongoId),
columns: { applicationStatus: true },
},
mysql: {
where: applyFilter(mysql.mysqlId),
columns: { applicationStatus: true },
},
postgres: {
where: applyFilter(postgres.postgresId),
columns: { applicationStatus: true },
},
redis: {
where: applyFilter(redis.redisId),
columns: { applicationStatus: true },
},
},
},
},
});
let applicationsCount = 0;
let composeCount = 0;
let databasesCount = 0;
let environmentsCount = 0;
const status = { running: 0, error: 0, idle: 0 };
const bump = (s?: string | null) => {
if (s === "done") status.running++;
else if (s === "error") status.error++;
else status.idle++;
};
for (const project of rows) {
for (const env of project.environments) {
environmentsCount++;
applicationsCount += env.applications.length;
composeCount += env.compose.length;
databasesCount +=
env.libsql.length +
env.mariadb.length +
env.mongo.length +
env.mysql.length +
env.postgres.length +
env.redis.length;
for (const a of env.applications) bump(a.applicationStatus);
for (const c of env.compose) bump(c.composeStatus);
for (const s of env.libsql) bump(s.applicationStatus);
for (const s of env.mariadb) bump(s.applicationStatus);
for (const s of env.mongo) bump(s.applicationStatus);
for (const s of env.mysql) bump(s.applicationStatus);
for (const s of env.postgres) bump(s.applicationStatus);
for (const s of env.redis) bump(s.applicationStatus);
}
}
return {
projects: rows.length,
environments: environmentsCount,
applications: applicationsCount,
compose: composeCount,
databases: databasesCount,
services: applicationsCount + composeCount + databasesCount,
status,
};
}),
search: protectedProcedure
.input(
z.object({
@@ -30,13 +30,9 @@ export const findPreviewDeploymentById = async (
with: {
domain: true,
application: {
with: {
server: true,
environment: {
with: {
project: true,
},
},
columns: {
applicationId: true,
serverId: true,
},
},
},