Skip to content

Commit 8823cb4

Browse files
authored
Merge pull request #45 from tinybirdco/vercel-dash
add pipes
2 parents 10c4fd5 + 7fd1df0 commit 8823cb4

16 files changed

+617
-13
lines changed

apps/web/src/components/tools/auth0/dashboard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useQueryState } from 'nuqs'
44
import { useEffect, useState, useCallback } from 'react'
55
import { pipe } from '@/lib/tinybird'
6-
import MetricCard from './metric'
6+
import MetricCard from '@/components/tools/shared/metric-card'
77
import { DauChart, DauDataPoint } from './dau-chart'
88
import { AuthMechChart, AuthMechDataPoint } from './auth-mech-chart'
99
import { DailySignupsChart, DailySignupsDataPoint } from './daily-signups-chart'
@@ -341,17 +341,17 @@ export default function Auth0Dashboard() {
341341
<MetricCard
342342
title="Monthly Sign Ups"
343343
value={summaryMetrics.monthly_signups}
344-
description="New users signed up in the last 30 days"
344+
isLoading={isLoading}
345345
/>
346346
<MetricCard
347347
title="Monthly Active Users"
348348
value={summaryMetrics.monthly_active_users}
349-
description="Users active in the last 30 days"
349+
isLoading={isLoading}
350350
/>
351351
<MetricCard
352352
title="New Signups Rate"
353353
value={`${summaryMetrics.conversion_rate}%`}
354-
description="New users compared to total users"
354+
isLoading={isLoading}
355355
/>
356356
</div>
357357
<Card>

apps/web/src/components/tools/clerk/dashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useQueryState } from 'nuqs'
44
import { useEffect, useState } from 'react'
55
import { pipe } from '@/lib/tinybird'
6-
import MetricCard from '../auth0/metric'
6+
import MetricCard from '@/components/tools/shared/metric-card'
77
import { DauChart } from './dau-chart'
88
import { AuthMechChart } from './auth-mech-chart'
99

apps/web/src/components/tools/orb/dashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useQueryState } from 'nuqs'
44
import { useEffect, useState } from 'react'
55
import { pipe } from '@/lib/tinybird'
6-
import MetricCard from '../auth0/metric'
6+
import MetricCard from '@/components/tools/shared/metric-card'
77
import { SubsChart } from './subs-chart'
88

99
interface SubsDataPoint {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
2+
3+
interface MetricCardProps {
4+
title: string
5+
value: string | number
6+
description?: string
7+
isLoading?: boolean
8+
}
9+
10+
export default function MetricCard({ title, value, description, isLoading }: MetricCardProps) {
11+
return (
12+
<Card>
13+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
14+
<CardTitle className="text-sm font-medium">{title}</CardTitle>
15+
</CardHeader>
16+
<CardContent>
17+
<div className="text-2xl font-bold">
18+
{isLoading ? 'Loading...' : value}
19+
</div>
20+
{description && (
21+
<p className="text-xs text-muted-foreground">{description}</p>
22+
)}
23+
</CardContent>
24+
</Card>
25+
)
26+
}

apps/web/src/components/tools/vercel/dashboard.tsx

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,178 @@
11
"use client"
22

33
import { useQueryState } from 'nuqs'
4-
import { useEffect } from 'react'
4+
import { useEffect, useState } from 'react'
5+
import { addDays, format } from 'date-fns'
6+
import { DateRange } from 'react-day-picker'
7+
import { pipe } from '@/lib/tinybird'
8+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
9+
import { TimeRange } from '@/components/tools/shared/time-range'
10+
import MetricCard from '@/components/tools/shared/metric-card'
11+
import { DeploymentsChart } from './deployments-chart'
12+
import { DurationChart } from './duration-chart'
13+
import { ProjectsChart } from './projects-chart'
14+
import { GitAnalyticsChart } from './git-analytics-chart'
15+
16+
interface GitData {
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
analytics: any[]
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
distribution: any[]
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22+
branches: any[]
23+
}
524

625
export default function VercelDashboard() {
726
const [token] = useQueryState('token')
27+
const [timeRange, setTimeRange] = useState('daily')
28+
const [dateRange, setDateRange] = useState<DateRange>({
29+
from: addDays(new Date(), -7),
30+
to: new Date()
31+
})
32+
33+
const [isLoading, setIsLoading] = useState(true)
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
const [metrics, setMetrics] = useState<any>()
36+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
37+
const [deploymentsData, setDeploymentsData] = useState<any[]>([])
38+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39+
const [durationData, setDurationData] = useState<any[]>([])
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
const [projectsData, setProjectsData] = useState<any[]>([])
42+
const [gitData, setGitData] = useState<GitData>({
43+
analytics: [],
44+
distribution: [],
45+
branches: []
46+
})
847

948
useEffect(() => {
10-
async function fetchMetrics() {
49+
async function fetchData() {
1150
if (!token) return
1251

52+
const params = {
53+
time_range: timeRange,
54+
...(dateRange?.from && { date_from: format(dateRange.from, 'yyyy-MM-dd HH:mm:ss') }),
55+
...(dateRange?.to && { date_to: format(dateRange.to, 'yyyy-MM-dd 23:59:59') })
56+
}
57+
1358
try {
14-
const [] = await Promise.all([
59+
setIsLoading(true)
60+
const [
61+
metricsResult,
62+
deploymentsResult,
63+
durationResult,
64+
projectsResult,
65+
gitAnalyticsResult,
66+
gitDistributionResult,
67+
branchResult,
68+
] = await Promise.all([
69+
pipe(token, 'vercel_deployment_metrics', params),
70+
pipe(token, 'vercel_deployments_over_time', params),
71+
pipe(token, 'vercel_deployment_duration', params),
72+
pipe(token, 'vercel_project_stats', params),
73+
pipe(token, 'vercel_git_analytics', params),
74+
pipe(token, 'vercel_git_distribution', params),
75+
pipe(token, 'vercel_branch_distribution', params),
1576
])
77+
78+
setMetrics(metricsResult?.data?.[0])
79+
setDeploymentsData(deploymentsResult?.data ?? [])
80+
setDurationData(durationResult?.data ?? [])
81+
setProjectsData(projectsResult?.data ?? [])
82+
setGitData({
83+
analytics: gitAnalyticsResult?.data ?? [],
84+
distribution: gitDistributionResult?.data ?? [],
85+
branches: branchResult?.data ?? []
86+
})
1687
} catch (error) {
17-
console.error('Failed to fetch metrics:', error)
88+
console.error('Failed to fetch data:', error)
89+
} finally {
90+
setIsLoading(false)
1891
}
1992
}
2093

21-
fetchMetrics()
22-
}, [token])
94+
fetchData()
95+
}, [token, timeRange, dateRange])
2396

2497
return (
2598
<div className="space-y-8">
99+
<TimeRange
100+
timeRange={timeRange}
101+
onTimeRangeChange={setTimeRange}
102+
dateRange={dateRange}
103+
onDateRangeChange={(range) => setDateRange(range || { from: addDays(new Date(), -7), to: new Date() })}
104+
className="mb-8"
105+
/>
106+
26107
{/* Metrics Row */}
27-
<div className="grid gap-4 md:grid-cols-3">
108+
<div className="grid gap-4 md:grid-cols-4">
109+
<MetricCard
110+
title="Total Deployments"
111+
value={metrics?.total_deployments ?? 'N/A'}
112+
/>
113+
<MetricCard
114+
title="Success Rate"
115+
value={metrics?.success_rate ? `${metrics.success_rate}%` : 'N/A'}
116+
/>
117+
<MetricCard
118+
title="Average Deploy Time"
119+
value={durationData.length > 0 ? `${Math.round(durationData.reduce((acc, curr) => acc + curr.avg_duration, 0) / durationData.length)}s` : 'N/A'}
120+
/>
121+
<MetricCard
122+
title="Error Rate"
123+
value={metrics?.error_rate ? `${metrics.error_rate}%` : 'N/A'}
124+
/>
28125
</div>
29126

30127
{/* Charts Grid */}
31128
<div className="grid gap-4 md:grid-cols-2">
129+
<Card>
130+
<CardHeader>
131+
<CardTitle>Deployments Over Time</CardTitle>
132+
</CardHeader>
133+
<CardContent>
134+
<DeploymentsChart
135+
data={deploymentsData}
136+
isLoading={isLoading}
137+
/>
138+
</CardContent>
139+
</Card>
140+
141+
<Card>
142+
<CardHeader>
143+
<CardTitle>Deploy Duration</CardTitle>
144+
</CardHeader>
145+
<CardContent>
146+
<DurationChart data={durationData} />
147+
</CardContent>
148+
</Card>
149+
</div>
150+
151+
{/* Tables and Additional Charts */}
152+
<div className="grid gap-4 md:grid-cols-2">
153+
<Card>
154+
<CardHeader>
155+
<CardTitle>Top Projects</CardTitle>
156+
</CardHeader>
157+
<CardContent>
158+
<ProjectsChart
159+
data={projectsData}
160+
isLoading={isLoading}
161+
/>
162+
</CardContent>
163+
</Card>
164+
165+
<Card>
166+
<CardHeader>
167+
<CardTitle>Git Analytics</CardTitle>
168+
</CardHeader>
169+
<CardContent>
170+
<GitAnalyticsChart
171+
data={gitData.analytics}
172+
isLoading={isLoading}
173+
/>
174+
</CardContent>
175+
</Card>
32176
</div>
33177
</div>
34178
)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { BarChart, Bar, XAxis, YAxis } from 'recharts'
2+
import { ChartContainer, ChartTooltip, ChartConfig } from '@/components/ui/chart'
3+
import { format } from 'date-fns'
4+
5+
interface DeploymentsData {
6+
period: string
7+
event_type: string
8+
count: number
9+
}
10+
11+
const chartConfig = {
12+
'deployment.succeeded': {
13+
color: "hsl(var(--primary))",
14+
label: "Successful Deployments",
15+
},
16+
'deployment.error': {
17+
color: "hsl(var(--secondary))",
18+
label: "Failed Deployments",
19+
},
20+
} satisfies ChartConfig
21+
22+
export function DeploymentsChart({ data, isLoading, className }: {
23+
data: DeploymentsData[]
24+
isLoading: boolean
25+
className?: string
26+
}) {
27+
if (isLoading) return <div className={`flex items-center justify-center ${className}`}>Loading...</div>
28+
if (!data.length) return <div className={`flex items-center justify-center ${className}`}>No data available</div>
29+
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
const chartData = data.reduce((acc: any[], curr: DeploymentsData) => {
32+
const existing = acc.find(d => d.period === curr.period)
33+
if (existing) {
34+
existing[curr.event_type] = curr.count
35+
} else {
36+
acc.push({
37+
period: curr.period,
38+
[curr.event_type]: curr.count
39+
})
40+
}
41+
return acc
42+
}, [])
43+
44+
return (
45+
<ChartContainer config={chartConfig} className={`w-full ${className}`}>
46+
<BarChart
47+
data={chartData}
48+
margin={{ left: 24, right: 24, top: 24, bottom: 24 }}
49+
>
50+
<XAxis
51+
dataKey="period"
52+
tickLine={false}
53+
axisLine={false}
54+
tickFormatter={(value) => format(new Date(value), 'd HH:mm')}
55+
/>
56+
<YAxis
57+
tickLine={false}
58+
axisLine={false}
59+
/>
60+
<ChartTooltip
61+
content={({ active, payload }) => {
62+
if (!active || !payload?.length) return null
63+
const data = payload[0].payload
64+
return (
65+
<div className="rounded-lg border bg-background p-2 shadow-sm">
66+
<div className="text-xs text-muted-foreground">
67+
{format(new Date(data.period), 'd HH:mm')}
68+
</div>
69+
{
70+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71+
payload.map((p: any) => (
72+
<div key={p.dataKey} className="font-bold">
73+
{chartConfig[p.dataKey as keyof typeof chartConfig].label}: {p.value}
74+
</div>
75+
))}
76+
</div>
77+
)
78+
}}
79+
/>
80+
<Bar
81+
dataKey="deployment.succeeded"
82+
fill={chartConfig['deployment.succeeded'].color}
83+
radius={[4, 4, 0, 0]}
84+
stackId="stack"
85+
/>
86+
<Bar
87+
dataKey="deployment.error"
88+
fill={chartConfig['deployment.error'].color}
89+
radius={[4, 4, 0, 0]}
90+
stackId="stack"
91+
/>
92+
</BarChart>
93+
</ChartContainer>
94+
)
95+
}

0 commit comments

Comments
 (0)