-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
304 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
apps/webservice/src/app/[workspaceSlug]/(app)/deployments/Card.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
"use client"; | ||
|
||
import { formatDistanceToNowStrict, subWeeks } from "date-fns"; | ||
import prettyMilliseconds from "pretty-ms"; | ||
|
||
import { Card } from "@ctrlplane/ui/card"; | ||
import { | ||
Table, | ||
TableBody, | ||
TableHead, | ||
TableHeader, | ||
TableRow, | ||
} from "@ctrlplane/ui/table"; | ||
|
||
import { api } from "~/trpc/react"; | ||
|
||
const DeploymentHistoryGraph: React.FC<{ name: string; repo: string }> = () => { | ||
const history = [ | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: null }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
{ successRate: 100 }, | ||
]; | ||
|
||
return ( | ||
<div className="flex h-[30px] items-center gap-1"> | ||
{history.map(({ successRate }, j) => ( | ||
<div | ||
key={j} | ||
className="relative h-full w-1.5 overflow-hidden rounded-sm" | ||
> | ||
{successRate == null ? ( | ||
<div className="absolute bottom-0 h-full w-full bg-neutral-700" /> | ||
) : ( | ||
<> | ||
<div className="absolute bottom-0 h-full w-full bg-red-500" /> | ||
<div | ||
className="absolute bottom-0 w-full bg-green-500" | ||
style={{ height: `${successRate}%` }} | ||
/> | ||
</> | ||
)} | ||
</div> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export const DeploymentsCard: React.FC<{ workspaceId: string }> = ({ | ||
workspaceId, | ||
}) => { | ||
const today = new Date(); | ||
const startDate = subWeeks(today, 2); | ||
const deployments = api.deployment.stats.byWorkspaceId.useQuery({ | ||
workspaceId, | ||
startDate, | ||
endDate: today, | ||
}); | ||
|
||
return ( | ||
<Card> | ||
<div> | ||
<div className="relative w-full overflow-auto"> | ||
<Table> | ||
<TableHeader> | ||
<TableRow className="h-16 hover:bg-transparent"> | ||
<TableHead className="p-4">Workflow</TableHead> | ||
<TableHead className="p-4">History (30 days)</TableHead> | ||
<TableHead className="w-[75px] p-4 xl:w-[150px]"> | ||
P50 Duration | ||
</TableHead> | ||
<TableHead className="w-[75px] p-4 xl:w-[150px]"> | ||
P90 Duration | ||
</TableHead> | ||
|
||
<TableHead className="w-[140px] p-4">Success Rate</TableHead> | ||
<TableHead className="hidden p-4 xl:table-cell xl:w-[120px]"> | ||
Last Run | ||
</TableHead> | ||
</TableRow> | ||
</TableHeader> | ||
|
||
<TableBody> | ||
{deployments.data?.map((deployment) => ( | ||
<tr key={deployment.id} className="border-b"> | ||
<td className="p-4 align-middle"> | ||
<div className="flex items-center gap-2"> | ||
{deployment.name} | ||
</div> | ||
<div className="text-xs text-muted-foreground"> | ||
{deployment.systemName} / {deployment.name} | ||
</div> | ||
</td> | ||
|
||
<td className="p-4 align-middle"> | ||
<DeploymentHistoryGraph | ||
name={deployment.name} | ||
repo={deployment.systemName} | ||
/> | ||
</td> | ||
|
||
<td className="p-4 "> | ||
{prettyMilliseconds(Math.round(deployment.p50) * 1000)} | ||
</td> | ||
<td className="p-4 "> | ||
{prettyMilliseconds(Math.round(deployment.p90) * 1000)} | ||
</td> | ||
|
||
<td className="p-4"> | ||
<div className="flex items-center gap-2"> | ||
<div className="h-2 w-full rounded-full bg-neutral-800"> | ||
<div | ||
className="h-full rounded-full bg-white transition-all" | ||
style={{ | ||
width: `${(deployment.totalSuccess / deployment.totalJobs) * 100}%`, | ||
}} | ||
/> | ||
</div> | ||
<div className="w-[75px] text-right"> | ||
{( | ||
(deployment.totalSuccess / deployment.totalJobs) * | ||
100 | ||
).toFixed(0)} | ||
% | ||
</div> | ||
</div> | ||
</td> | ||
|
||
<td className="hidden p-4 align-middle xl:table-cell"> | ||
<div> | ||
{formatDistanceToNowStrict(new Date(), { | ||
addSuffix: false, | ||
})} | ||
</div> | ||
</td> | ||
</tr> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</div> | ||
</div> | ||
</Card> | ||
); | ||
}; |
41 changes: 41 additions & 0 deletions
41
apps/webservice/src/app/[workspaceSlug]/(app)/deployments/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import type { Metadata } from "next"; | ||
import { notFound } from "next/navigation"; | ||
import { IconRocket } from "@tabler/icons-react"; | ||
|
||
import { api } from "~/trpc/server"; | ||
import { DeploymentsCard } from "./Card"; | ||
|
||
export const metadata: Metadata = { | ||
title: "Deployments | Ctrlplane", | ||
}; | ||
|
||
export default async function DeploymentsPage({ | ||
params, | ||
}: { | ||
params: { workspaceSlug: string }; | ||
}) { | ||
const { workspaceSlug } = params; | ||
const workspace = await api.workspace.bySlug(workspaceSlug); | ||
if (workspace == null) notFound(); | ||
return ( | ||
<div> | ||
<div className="flex items-center gap-2 border-b px-2"> | ||
<div className="flex items-center gap-2 p-3"> | ||
<IconRocket className="h-4 w-4" /> Deployments | ||
</div> | ||
</div> | ||
|
||
<div className="container m-8 mx-auto"> | ||
<div> | ||
<div className="mb-4"> | ||
<h2 className="text-xl font-semibold">Billing</h2> | ||
<p className="text-sm text-muted-foreground"> | ||
Monitor your usage costs, credits, and billing status | ||
</p> | ||
</div> | ||
</div> | ||
<DeploymentsCard workspaceId={workspace.id} /> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { z } from "zod"; | ||
|
||
import { and, count, eq, gte, lte, max, sql } from "@ctrlplane/db"; | ||
import { | ||
deployment, | ||
job, | ||
release, | ||
releaseJobTrigger, | ||
system, | ||
} from "@ctrlplane/db/schema"; | ||
import { Permission } from "@ctrlplane/validators/auth"; | ||
|
||
import { createTRPCRouter, protectedProcedure } from "../trpc"; | ||
|
||
export const deploymentStatsRouter = createTRPCRouter({ | ||
byWorkspaceId: protectedProcedure | ||
.meta({ | ||
authorizationCheck: ({ canUser, input }) => | ||
canUser | ||
.perform(Permission.DeploymentList) | ||
.on({ type: "workspace", id: input.workspaceId }), | ||
}) | ||
.input( | ||
z.object({ | ||
workspaceId: z.string().uuid(), | ||
startDate: z.date(), | ||
endDate: z.date(), | ||
}), | ||
) | ||
.query(({ ctx, input }) => | ||
ctx.db | ||
.select({ | ||
id: deployment.id, | ||
name: deployment.name, | ||
slug: deployment.slug, | ||
systemId: system.id, | ||
systemSlug: system.slug, | ||
systemName: system.name, | ||
lastRunAt: max(job.createdAt), | ||
totalJobs: count(job.id), | ||
totalSuccess: sql<number>` | ||
(COUNT(*) FILTER (WHERE ${job.status} = 'completed') / COUNT(*)) * 100 | ||
`, | ||
|
||
p50: sql<number>` | ||
percentile_cont(0.5) within group (order by | ||
EXTRACT(EPOCH FROM (${job.completedAt} - ${job.startedAt})) | ||
) | ||
`, | ||
|
||
p90: sql<number>` | ||
percentile_cont(0.9) within group (order by | ||
EXTRACT(EPOCH FROM (${job.completedAt} - ${job.startedAt})) | ||
) | ||
`, | ||
}) | ||
.from(deployment) | ||
.innerJoin(release, eq(release.deploymentId, deployment.id)) | ||
.innerJoin( | ||
releaseJobTrigger, | ||
eq(releaseJobTrigger.releaseId, release.id), | ||
) | ||
.innerJoin(job, eq(job.id, releaseJobTrigger.jobId)) | ||
.innerJoin(system, eq(system.id, deployment.systemId)) | ||
.where( | ||
and( | ||
eq(system.workspaceId, input.workspaceId), | ||
gte(job.createdAt, input.startDate), | ||
lte(job.createdAt, input.endDate), | ||
), | ||
) | ||
.groupBy( | ||
deployment.id, | ||
deployment.name, | ||
deployment.slug, | ||
system.id, | ||
system.name, | ||
system.slug, | ||
) | ||
.limit(100), | ||
), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters