From de9f31f36ba5ac1f4e36eff9cb2e737afdf103c1 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari <48932219+adityachoudhari26@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:46:05 -0700 Subject: [PATCH] fix: Show different statuses on graph (#156) --- .../systems/JobHistoryChart.tsx | 107 +++++++++++++----- packages/api/src/router/job.ts | 25 +++- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx b/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx index ea65ce16..c5fbc650 100644 --- a/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/systems/JobHistoryChart.tsx @@ -4,6 +4,7 @@ import type { Workspace } from "@ctrlplane/db/schema"; import { isSameDay, startOfDay, sub } from "date-fns"; import _ from "lodash"; import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; +import colors from "tailwindcss/colors"; import { CardContent, @@ -11,25 +12,42 @@ import { CardHeader, CardTitle, } from "@ctrlplane/ui/card"; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@ctrlplane/ui/chart"; +import { ChartContainer, ChartTooltip } from "@ctrlplane/ui/chart"; import { JobStatus } from "@ctrlplane/validators/jobs"; import { api } from "~/trpc/react"; import { dateRange } from "~/utils/date/range"; +const statusColors = { + [JobStatus.ActionRequired]: colors.yellow[500], + [JobStatus.ExternalRunNotFound]: colors.red[700], + [JobStatus.InvalidIntegration]: colors.amber[700], + [JobStatus.InvalidJobAgent]: colors.amber[400], + [JobStatus.Failure]: colors.red[500], + [JobStatus.Cancelled]: colors.neutral[600], + [JobStatus.Skipped]: colors.neutral[500], + [JobStatus.Pending]: colors.neutral[400], + [JobStatus.InProgress]: colors.blue[500], + [JobStatus.Completed]: colors.green[500], +}; + +const statusLabels = { + [JobStatus.ActionRequired]: "Action Required", + [JobStatus.ExternalRunNotFound]: "External Run Not Found", + [JobStatus.InvalidIntegration]: "Invalid Integration", + [JobStatus.InvalidJobAgent]: "Invalid Job Agent", + [JobStatus.Failure]: "Failure", + [JobStatus.Cancelled]: "Cancelled", + [JobStatus.Skipped]: "Skipped", + [JobStatus.Pending]: "Pending", + [JobStatus.InProgress]: "In Progress", + [JobStatus.Completed]: "Completed", +}; + export const JobHistoryChart: React.FC<{ workspace: Workspace; className?: string; }> = ({ className, workspace }) => { - 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, @@ -37,10 +55,12 @@ export const JobHistoryChart: React.FC<{ const now = startOfDay(new Date()); const chartData = dateRange(sub(now, { weeks: 6 }), now, 1, "days").map( - (d) => ({ - date: new Date(d).toISOString(), - jobs: dailyCounts.data?.find((c) => isSameDay(c.date, d))?.count ?? 0, - }), + (d) => { + const dayData = + dailyCounts.data?.find((c) => isSameDay(c.date, d))?.statusCounts ?? {}; + const date = new Date(d).toISOString(); + return { date, ...dayData }; + }, ); const targets = api.target.byWorkspaceId.list.useQuery({ @@ -48,6 +68,11 @@ export const JobHistoryChart: React.FC<{ }); const deployments = api.deployment.byWorkspaceId.useQuery(workspace.id, {}); + const totalJobs = dailyCounts.data?.reduce( + (acc, c) => acc + Number(c.totalCount), + 0, + ); + return (
@@ -61,9 +86,7 @@ export const JobHistoryChart: React.FC<{
Jobs - {releaseJobTriggers.data?.filter( - (t) => t.job.status !== JobStatus.Pending, - ).length ?? "-"} + {totalJobs ?? "-"}
@@ -114,21 +137,43 @@ export const JobHistoryChart: React.FC<{ }} /> { - return new Date(value).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); - }} - /> - } + content={({ active, payload, label }) => { + if (active && payload?.length) + return ( +
+
+ {new Date(label).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} +
+ {payload.reverse().map((entry, index) => ( +
+
+ {statusLabels[entry.name as JobStatus]}: + {entry.value} +
+ ))} +
+ ); + return null; + }} /> - + {Object.entries(statusColors).map(([status, color]) => ( + + ))} diff --git a/packages/api/src/router/job.ts b/packages/api/src/router/job.ts index b2ff3bd8..37519f8d 100644 --- a/packages/api/src/router/job.ts +++ b/packages/api/src/router/job.ts @@ -85,20 +85,37 @@ const releaseJobTriggerRouter = createTRPCRouter({ }) .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 + + const subquery = ctx.db .select({ date: dateTruncExpr.as("date"), - count: sql`COUNT(*)`.as("count"), + status: job.status, + countPerStatus: sql`COUNT(*)`.as("countPerStatus"), }) .from(releaseJobTrigger) + .innerJoin(job, eq(releaseJobTrigger.jobId, job.id)) .innerJoin( environment, eq(releaseJobTrigger.environmentId, environment.id), ) .innerJoin(system, eq(environment.systemId, system.id)) .where(eq(system.workspaceId, input.workspaceId)) - .groupBy(dateTruncExpr) - .orderBy(dateTruncExpr); + .groupBy(dateTruncExpr, job.status) + .as("sub"); + + return ctx.db + .select({ + date: subquery.date, + totalCount: sql`SUM(${subquery.countPerStatus})`.as( + "totalCount", + ), + statusCounts: sql>` + jsonb_object_agg(${subquery.status}, ${subquery.countPerStatus}) + `.as("statusCounts"), + }) + .from(subquery) + .groupBy(subquery.date) + .orderBy(subquery.date); }), }),