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({