Skip to content

Commit 6b052f7

Browse files
committed
auth0 pipes & dashboard
1 parent bba9349 commit 6b052f7

File tree

10 files changed

+392
-105
lines changed

10 files changed

+392
-105
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
const TINYBIRD_API_URL = "https://api.tinybird.co/v0/pipes";
4+
5+
export async function GET(request: NextRequest) {
6+
const searchParams = request.nextUrl.searchParams;
7+
const token = searchParams.get("token");
8+
const pipeName = searchParams.get("pipe");
9+
10+
if (!token || !pipeName) {
11+
return NextResponse.json(
12+
{ error: "Missing required parameters" },
13+
{ status: 400 }
14+
);
15+
}
16+
17+
const url = new URL(`${TINYBIRD_API_URL}/${pipeName}.json`);
18+
19+
// Forward all query parameters except token and pipe
20+
searchParams.forEach((value, key) => {
21+
if (key !== "token" && key !== "pipe") {
22+
url.searchParams.append(key, value);
23+
}
24+
});
25+
26+
try {
27+
const response = await fetch(url, {
28+
headers: {
29+
Authorization: `Bearer ${token}`,
30+
},
31+
});
32+
33+
const data = await response.json();
34+
return NextResponse.json(data);
35+
} catch (error) {
36+
return NextResponse.json(
37+
{ error: "Failed to fetch data from Tinybird" },
38+
{ status: 500 }
39+
);
40+
}
41+
}
42+
43+
export async function POST(request: NextRequest) {
44+
const { token, pipe, ...params } = await request.json();
45+
46+
if (!token || !pipe) {
47+
return NextResponse.json(
48+
{ error: "Missing required parameters" },
49+
{ status: 400 }
50+
);
51+
}
52+
53+
const url = new URL(`${TINYBIRD_API_URL}/${pipe}.json`);
54+
55+
// Add all params as query parameters
56+
Object.entries(params).forEach(([key, value]) => {
57+
url.searchParams.append(key, String(value));
58+
});
59+
60+
try {
61+
const response = await fetch(url, {
62+
headers: {
63+
Authorization: `Bearer ${token}`,
64+
},
65+
});
66+
67+
const data = await response.json();
68+
return NextResponse.json(data);
69+
} catch (error) {
70+
return NextResponse.json(
71+
{ error: "Failed to fetch data from Tinybird" },
72+
{ status: 500 }
73+
);
74+
}
75+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client"
2+
3+
import {
4+
Card,
5+
CardContent,
6+
CardHeader,
7+
CardTitle,
8+
} from "@/components/ui/card"
9+
import {
10+
ChartContainer,
11+
ChartTooltip,
12+
ChartConfig,
13+
ChartTooltipContent
14+
} from "@/components/ui/chart"
15+
import { Bar, BarChart, XAxis, YAxis } from "recharts"
16+
17+
interface AuthMechData {
18+
mech: string
19+
logins: number
20+
}
21+
22+
interface AuthMechChartProps {
23+
data: AuthMechData[]
24+
}
25+
26+
const chartConfig = {
27+
logins: {
28+
color: "hsl(var(--primary))",
29+
label: "Logins",
30+
},
31+
} satisfies ChartConfig
32+
33+
export function AuthMechChart({ data }: AuthMechChartProps) {
34+
// Sort data by number of logins in descending order
35+
const sortedData = [...data].sort((a, b) => b.logins - a.logins)
36+
37+
return (
38+
<Card>
39+
<CardHeader>
40+
<CardTitle>Authentication Methods</CardTitle>
41+
</CardHeader>
42+
<CardContent>
43+
<ChartContainer config={chartConfig}>
44+
<BarChart
45+
data={sortedData}
46+
layout="vertical"
47+
margin={{
48+
left: 80,
49+
right: 12,
50+
top: 12,
51+
bottom: 12,
52+
}}
53+
>
54+
<XAxis
55+
type="number"
56+
tickLine={false}
57+
axisLine={false}
58+
tickMargin={8}
59+
/>
60+
<YAxis
61+
type="category"
62+
dataKey="mech"
63+
tickLine={false}
64+
axisLine={false}
65+
tickMargin={8}
66+
width={70}
67+
/>
68+
<ChartTooltip
69+
content={<ChartTooltipContent />}
70+
cursor={{ fill: "hsl(var(--muted))", opacity: 0.2 }}
71+
/>
72+
<Bar
73+
dataKey="logins"
74+
radius={[4, 4, 4, 4]}
75+
fill="hsl(var(--primary))"
76+
/>
77+
</BarChart>
78+
</ChartContainer>
79+
</CardContent>
80+
</Card>
81+
)
82+
}

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

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,46 @@ import { useQueryState } from 'nuqs'
44
import { useEffect, useState } from 'react'
55
import Link from 'next/link'
66
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
7-
import { query } from '@/lib/tinybird'
7+
import { pipe } from '@/lib/tinybird'
88
import MetricCard from './metric'
9+
import { DauChart } from './dau-chart'
10+
import { AuthMechChart } from './auth-mech-chart'
11+
12+
interface DauDataPoint {
13+
day: string
14+
active: number
15+
}
16+
17+
interface AuthMechData {
18+
mech: string
19+
logins: number
20+
}
921

1022
export default function Auth0Dashboard() {
1123
const [token] = useQueryState('token')
12-
const [totalUsers, setTotalUsers] = useState<number>(0)
13-
const [activeUsers, setActiveUsers] = useState<number>(0)
24+
const [monthlySignUps, setMonthlySignUps] = useState<number>(0)
25+
const [monthlyMau, setMonthlyMau] = useState<number>(0)
1426
const [conversionRate, setConversionRate] = useState<number>(0)
27+
const [dauData, setDauData] = useState<DauDataPoint[]>([])
28+
const [authMechData, setAuthMechData] = useState<AuthMechData[]>([])
1529

1630
useEffect(() => {
1731
async function fetchMetrics() {
1832
if (!token) return
1933

2034
try {
21-
const [totalResult, activeResult] = await Promise.all([
22-
query(token, "SELECT count() as total FROM auth0_logs WHERE type = 'ss' FORMAT JSON"),
23-
query(token, "SELECT count(DISTINCT user_id) as active FROM auth0_logs where type == 's' and date >= now() - interval 30 days FORMAT JSON")
35+
const [monthlySignUpsResult, monthlyMauResult, dauResult, authMechResult] = await Promise.all([
36+
pipe(token, 'auth0_signups'),
37+
pipe(token, 'auth0_mau'),
38+
pipe<{ data: DauDataPoint[] }>(token, 'auth0_dau_ts'),
39+
pipe<{ data: AuthMechData[] }>(token, 'auth0_mech_usage')
2440
])
2541

26-
const total = totalResult.data[0]?.total || 0
27-
const active = activeResult.data[0]?.active || 0
28-
29-
setTotalUsers(total)
30-
setActiveUsers(active)
31-
setConversionRate(total > 0 ? Math.round((active / total) * 100) : 0)
42+
setMonthlySignUps(monthlySignUpsResult.data[0]?.total || 0)
43+
setMonthlyMau(monthlyMauResult.data[0]?.active || 0)
44+
setConversionRate(0)
45+
setDauData(dauResult.data)
46+
setAuthMechData(authMechResult.data)
3247
} catch (error) {
3348
console.error('Failed to fetch metrics:', error)
3449
}
@@ -52,13 +67,13 @@ export default function Auth0Dashboard() {
5267
{/* Metrics Row */}
5368
<div className="grid gap-4 md:grid-cols-3">
5469
<MetricCard
55-
title="Total Users"
56-
value={totalUsers.toLocaleString()}
57-
description="Total registered users"
70+
title="Monthly Sign Ups"
71+
value={monthlySignUps.toLocaleString()}
72+
description="New users signed up in the last 30 days"
5873
/>
5974
<MetricCard
60-
title="Active Users (30d)"
61-
value={activeUsers.toLocaleString()}
75+
title="Monthly Active Users"
76+
value={monthlyMau.toLocaleString()}
6277
description="Users active in the last 30 days"
6378
/>
6479
<MetricCard
@@ -70,36 +85,20 @@ export default function Auth0Dashboard() {
7085

7186
{/* Charts Grid */}
7287
<div className="grid gap-4 md:grid-cols-2">
88+
<DauChart data={dauData} />
89+
<AuthMechChart data={authMechData} />
7390
<Card className="col-span-1">
7491
<CardHeader>
75-
<CardTitle>User Growth</CardTitle>
76-
</CardHeader>
77-
<CardContent className="h-[300px]">
78-
{/* User Growth Chart will go here */}
79-
</CardContent>
80-
</Card>
81-
<Card className="col-span-1">
82-
<CardHeader>
83-
<CardTitle>Daily Active Users</CardTitle>
84-
</CardHeader>
85-
<CardContent className="h-[300px]">
86-
{/* DAU Chart will go here */}
87-
</CardContent>
88-
</Card>
89-
<Card className="col-span-1">
90-
<CardHeader>
91-
<CardTitle>User Actions</CardTitle>
92+
<CardTitle>Something else</CardTitle>
9293
</CardHeader>
9394
<CardContent className="h-[300px]">
94-
{/* User Actions Chart will go here */}
9595
</CardContent>
9696
</Card>
9797
<Card className="col-span-1">
9898
<CardHeader>
99-
<CardTitle>Session Duration</CardTitle>
99+
<CardTitle>Something else</CardTitle>
100100
</CardHeader>
101101
<CardContent className="h-[300px]">
102-
{/* Session Duration Chart will go here */}
103102
</CardContent>
104103
</Card>
105104
</div>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use client"
2+
3+
import {
4+
Card,
5+
CardContent,
6+
CardHeader,
7+
CardTitle,
8+
} from "@/components/ui/card"
9+
import {
10+
ChartContainer,
11+
ChartTooltip,
12+
ChartConfig,
13+
ChartTooltipContent
14+
} from "@/components/ui/chart"
15+
import { Line, LineChart, XAxis } from "recharts"
16+
17+
interface DauDataPoint {
18+
day: string
19+
active: number
20+
}
21+
22+
interface DauChartData {
23+
data: DauDataPoint[]
24+
}
25+
26+
const chartConfig = {
27+
active: {
28+
color: "hsl(var(--primary))",
29+
label: "Active Users",
30+
},
31+
} satisfies ChartConfig
32+
33+
export function DauChart({ data }: DauChartData) {
34+
return (
35+
<Card>
36+
<CardHeader>
37+
<CardTitle>Daily Active Users</CardTitle>
38+
</CardHeader>
39+
<CardContent className="">
40+
<ChartContainer config={chartConfig} >
41+
<LineChart
42+
data={data}
43+
margin={{
44+
left: 12,
45+
right: 12,
46+
}}
47+
>
48+
<XAxis
49+
dataKey="day"
50+
tickLine={false}
51+
axisLine={false}
52+
tickMargin={8}
53+
interval="equidistantPreserveStart"
54+
tickFormatter={(value) => value.split('-')[2]}
55+
/>
56+
<ChartTooltip
57+
cursor={false}
58+
content={<ChartTooltipContent indicator="dot" />}
59+
/>
60+
{/* <ChartTooltip content={({ payload }) => {
61+
if (!payload?.length) return null
62+
63+
return (
64+
<div className="rounded-lg border bg-background p-2 shadow-sm">
65+
<div className="grid grid-cols-2 gap-2">
66+
<div className="flex flex-col">
67+
<span className="text-[0.70rem] uppercase text-muted-foreground">
68+
Date
69+
</span>
70+
<span className="font-bold text-muted-foreground">
71+
{payload[0].payload.day}
72+
</span>
73+
</div>
74+
<div className="flex flex-col">
75+
<span className="text-[0.70rem] uppercase text-muted-foreground">
76+
Active Users
77+
</span>
78+
<span className="font-bold">
79+
{payload[0].value}
80+
</span>
81+
</div>
82+
</div>
83+
</div>
84+
)
85+
}} /> */}
86+
<Line
87+
type="monotone"
88+
dataKey="active"
89+
strokeWidth={2}
90+
activeDot={{
91+
r: 4,
92+
style: { fill: "hsl(var(--primary))" },
93+
}}
94+
style={{
95+
stroke: "hsl(var(--primary))",
96+
}}
97+
/>
98+
</LineChart>
99+
</ChartContainer>
100+
</CardContent>
101+
</Card>
102+
)
103+
}

0 commit comments

Comments
 (0)