diff --git a/apps/webservice/src/app/[workspaceSlug]/(job)/jobs/page.tsx b/apps/webservice/src/app/[workspaceSlug]/(job)/jobs/page.tsx index 0b431ce8..629ca733 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(job)/jobs/page.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(job)/jobs/page.tsx @@ -21,7 +21,9 @@ export default async function JobsPage({ const workspace = await api.workspace.bySlug(params.workspaceSlug); if (workspace == null) return notFound(); - const releaseJobTriggers = await api.job.config.byWorkspaceId(workspace.id); + const releaseJobTriggers = await api.job.config.byWorkspaceId.list( + workspace.id, + ); if (releaseJobTriggers.length === 0) return ; diff --git a/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx b/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx index 99ee43bb..b8708662 100644 --- a/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx @@ -1,7 +1,7 @@ "use client"; import type { Workspace } from "@ctrlplane/db/schema"; -import { startOfDay, sub } from "date-fns"; +import { isSameDay, startOfDay, sub } from "date-fns"; import _ from "lodash"; import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; @@ -25,21 +25,21 @@ export const JobHistoryChart: React.FC<{ workspace: Workspace; className?: string; }> = ({ className, workspace }) => { - const releaseJobTriggers = api.job.config.byWorkspaceId.useQuery( + const releaseJobTriggers = api.job.config.byWorkspaceId.list.useQuery( workspace.id, { refetchInterval: 60_000 }, ); + const dailyCounts = api.job.config.byWorkspaceId.dailyCount.useQuery({ + workspaceId: workspace.id, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + const now = startOfDay(new Date()); const chartData = dateRange(sub(now, { weeks: 6 }), now, 1, "days").map( (d) => ({ - date: d.toString(), - jobs: (releaseJobTriggers.data ?? []).filter( - (j) => - j.job.createdAt != null && - j.job.status !== JobStatus.Pending && - startOfDay(j.job.createdAt).toString() === d.toString(), - ).length, + date: d, + jobs: dailyCounts.data?.find((c) => isSameDay(c.date, d))?.count ?? 0, }), ); diff --git a/packages/api/src/router/job.ts b/packages/api/src/router/job.ts index acc4e8c0..b2ff3bd8 100644 --- a/packages/api/src/router/job.ts +++ b/packages/api/src/router/job.ts @@ -3,7 +3,7 @@ import _ from "lodash"; import { isPresent } from "ts-is-present"; import { z } from "zod"; -import { and, asc, desc, eq, isNull, takeFirst } from "@ctrlplane/db"; +import { and, asc, desc, eq, isNull, sql, takeFirst } from "@ctrlplane/db"; import { createJobAgent, deployment, @@ -42,33 +42,65 @@ const releaseJobTriggerQuery = (tx: Tx) => .innerJoin(jobAgent, eq(jobAgent.id, deployment.jobAgentId)); const releaseJobTriggerRouter = createTRPCRouter({ - byWorkspaceId: protectedProcedure - .input(z.string().uuid()) - .meta({ - authorizationCheck: ({ canUser, input }) => - canUser - .perform(Permission.SystemList) - .on({ type: "workspace", id: input }), - }) - .query(({ ctx, input }) => - releaseJobTriggerQuery(ctx.db) - .leftJoin(system, eq(system.id, deployment.systemId)) - .where( - and(eq(system.workspaceId, input), isNull(environment.deletedAt)), - ) - .orderBy(asc(releaseJobTrigger.createdAt)) - .limit(1_000) - .then((data) => - data.map((t) => ({ - ...t.release_job_trigger, - job: t.job, - agent: t.job_agent, - target: t.target, - release: { ...t.release, deployment: t.deployment }, - environment: t.environment, - })), - ), - ), + byWorkspaceId: createTRPCRouter({ + list: protectedProcedure + .input(z.string().uuid()) + .meta({ + authorizationCheck: ({ canUser, input }) => + canUser + .perform(Permission.SystemList) + .on({ type: "workspace", id: input }), + }) + .query(({ ctx, input }) => + releaseJobTriggerQuery(ctx.db) + .leftJoin(system, eq(system.id, deployment.systemId)) + .where( + and(eq(system.workspaceId, input), isNull(environment.deletedAt)), + ) + .orderBy(asc(releaseJobTrigger.createdAt)) + .limit(1_000) + .then((data) => + data.map((t) => ({ + ...t.release_job_trigger, + job: t.job, + agent: t.job_agent, + target: t.target, + release: { ...t.release, deployment: t.deployment }, + environment: t.environment, + })), + ), + ), + dailyCount: protectedProcedure + .input( + z.object({ + workspaceId: z.string().uuid(), + timezone: z.string(), + }), + ) + .meta({ + authorizationCheck: ({ canUser, input }) => + canUser + .perform(Permission.SystemList) + .on({ type: "workspace", id: input.workspaceId }), + }) + .query(async ({ ctx, input }) => { + const dateTruncExpr = sql`date_trunc('day', ${releaseJobTrigger.createdAt} AT TIME ZONE 'UTC' AT TIME ZONE '${sql.raw(input.timezone)}')`; + return ctx.db + .select({ + date: dateTruncExpr.as("date"), + count: sql`COUNT(*)`.as("count"), + }) + .from(releaseJobTrigger) + .innerJoin( + environment, + eq(releaseJobTrigger.environmentId, environment.id), + ) + .innerJoin(system, eq(environment.systemId, system.id)) + .where(eq(system.workspaceId, input.workspaceId)) + .groupBy(dateTruncExpr) + .orderBy(dateTruncExpr); + }), + }), byDeploymentAndEnvironment: protectedProcedure .meta({