feat: implement homeStats query for dashboard overview

- Replace individual project and server queries with a consolidated homeStats query to streamline data retrieval for the dashboard.
- Update the ShowHome component to utilize homeStats for displaying project, environment, application, and service counts, along with their status breakdown.
- Enhance data handling for user permissions to ensure accurate statistics based on user access levels.
This commit is contained in:
Mauricio Siu
2026-04-17 22:18:14 -06:00
parent 2ba1df1eaa
commit 5c787adae1
2 changed files with 157 additions and 49 deletions
@@ -95,8 +95,7 @@ function StatusListCard({
export const ShowHome = () => {
const { data: auth } = api.user.get.useQuery();
const { data: projects } = api.project.all.useQuery();
const { data: servers } = api.server.all.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(
@@ -109,53 +108,19 @@ export const ShowHome = () => {
const firstName = auth?.user?.firstName?.trim();
const { totals, statusBreakdown } = useMemo(() => {
let applications = 0;
let compose = 0;
let databases = 0;
let environments = 0;
const breakdown = { running: 0, error: 0, idle: 0 };
const dbKeys = [
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"libsql",
] as const;
const bump = (status?: string) => {
if (status === "done") breakdown.running++;
else if (status === "error") breakdown.error++;
else breakdown.idle++;
};
for (const p of projects ?? []) {
for (const env of p.environments ?? []) {
environments++;
applications += env.applications?.length ?? 0;
compose += env.compose?.length ?? 0;
for (const a of env.applications ?? []) bump(a.applicationStatus);
for (const c of env.compose ?? []) bump((c as any).composeStatus);
for (const key of dbKeys) {
const list = ((env as any)[key] ?? []) as Array<{
applicationStatus?: string;
}>;
databases += list.length;
for (const s of list) bump(s.applicationStatus);
}
}
}
return {
totals: {
projects: projects?.length ?? 0,
environments,
applications,
compose,
databases,
services: applications + compose + databases,
},
statusBreakdown: breakdown,
};
}, [projects, servers]);
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 [];
+143
View File
@@ -487,6 +487,149 @@ 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({