Skip to content

Commit f03c34c

Browse files
committed
orb dash, installed/available section
1 parent 6b052f7 commit f03c34c

File tree

4 files changed

+270
-29
lines changed

4 files changed

+270
-29
lines changed

apps/web/src/app/orb/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import OrbDashboard from "@/components/tools/orb/dashboard";
2+
3+
export default function OrbPage() {
4+
return <OrbDashboard />;
5+
}

apps/web/src/app/page.tsx

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
'use client';
1+
"use client";
22

33
import { useQueryState } from 'nuqs';
44
import { Button } from '@/components/ui/button';
@@ -37,9 +37,56 @@ const KNOWN_APPS: AppGridItem[] = [
3737
name: 'Auth0',
3838
description: 'Identity platform',
3939
icon: '🔑'
40+
},
41+
{
42+
id: 'vercel',
43+
ds: 'vercel_logs',
44+
name: 'Vercel Logs',
45+
description: 'Deployment and serverless logs',
46+
icon: '📊'
47+
},
48+
{
49+
id: 'gitlab',
50+
ds: 'gitlab',
51+
name: 'Gitlab',
52+
description: 'Source code management',
53+
icon: '🦊'
54+
},
55+
{
56+
id: 'orb',
57+
ds: 'orb',
58+
name: 'Orb',
59+
description: 'Usage-based billing',
60+
icon: '💰'
4061
}
4162
];
4263

64+
function AppCard({ app, isInstalled, token }: { app: AppGridItem; isInstalled: boolean; token?: string }) {
65+
return (
66+
<Link
67+
key={app.id}
68+
href={`/${app.id}${token ? `?token=${token}` : ''}`}
69+
>
70+
<Card className="p-6 hover:shadow-lg transition-shadow cursor-pointer">
71+
<div className="flex items-center gap-4">
72+
<div className="text-4xl">{app.icon}</div>
73+
<div>
74+
<h2 className="text-xl font-semibold">{app.name}</h2>
75+
<p className="text-gray-500">{app.description}</p>
76+
<div className="mt-2">
77+
{isInstalled ? (
78+
<span className="text-green-500 text-sm">Installed</span>
79+
) : (
80+
<span className="text-gray-400 text-sm">Not installed</span>
81+
)}
82+
</div>
83+
</div>
84+
</div>
85+
</Card>
86+
</Link>
87+
);
88+
}
89+
4390
export default function Home() {
4491
const [token, setToken] = useQueryState('token');
4592
const [inputToken, setInputToken] = useState(token || '');
@@ -88,34 +135,32 @@ export default function Home() {
88135
);
89136
}
90137

138+
const installedAppsList = KNOWN_APPS.filter(app => installedApps.includes(app.ds));
139+
const uninstalledAppsList = KNOWN_APPS.filter(app => !installedApps.includes(app.ds));
140+
91141
return (
92-
<>
93-
<h1 className="text-3xl font-bold mb-8">Your Apps</h1>
94-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
95-
{KNOWN_APPS.map((app) => (
96-
<Link
97-
key={app.id}
98-
href={`/${app.id}${token ? `?token=${token}` : ''}`}
99-
>
100-
<Card className="p-6 hover:shadow-lg transition-shadow cursor-pointer">
101-
<div className="flex items-center gap-4">
102-
<div className="text-4xl">{app.icon}</div>
103-
<div>
104-
<h2 className="text-xl font-semibold">{app.name}</h2>
105-
<p className="text-gray-500">{app.description}</p>
106-
<div className="mt-2">
107-
{installedApps.includes(app.ds) ? (
108-
<span className="text-green-500 text-sm">Installed</span>
109-
) : (
110-
<span className="text-gray-400 text-sm">Not installed</span>
111-
)}
112-
</div>
113-
</div>
114-
</div>
115-
</Card>
116-
</Link>
117-
))}
118-
</div>
119-
</>
142+
<div className="space-y-8">
143+
{installedAppsList.length > 0 && (
144+
<div>
145+
<h2 className="text-2xl font-semibold mb-4">Installed Apps</h2>
146+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
147+
{installedAppsList.map((app) => (
148+
<AppCard key={app.id} app={app} isInstalled={true} token={token} />
149+
))}
150+
</div>
151+
</div>
152+
)}
153+
154+
{uninstalledAppsList.length > 0 && (
155+
<div>
156+
<h2 className="text-2xl font-semibold mb-4">Available Apps</h2>
157+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
158+
{uninstalledAppsList.map((app) => (
159+
<AppCard key={app.id} app={app} isInstalled={false} token={token} />
160+
))}
161+
</div>
162+
</div>
163+
)}
164+
</div>
120165
);
121166
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client"
2+
3+
import { useQueryState } from 'nuqs'
4+
import { useEffect, useState } from 'react'
5+
import Link from 'next/link'
6+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
7+
import { pipe } from '@/lib/tinybird'
8+
import MetricCard from '../auth0/metric'
9+
import { SubsChart } from './subs-chart'
10+
11+
interface SubsDataPoint {
12+
day: string
13+
invoices: number
14+
}
15+
16+
export default function OrbDashboard() {
17+
const [token] = useQueryState('token')
18+
const [newSubs, setNewSubs] = useState<number>(0)
19+
const [churnedSubs, setChurnedSubs] = useState<number>(0)
20+
const [issuedInvoices, setIssuedInvoices] = useState<number>(0)
21+
const [paidInvoices, setPaidInvoices] = useState<number>(0)
22+
const [subsTimeSeriesData, setSubsTimeSeriesData] = useState<SubsDataPoint[]>([])
23+
24+
useEffect(() => {
25+
async function fetchMetrics() {
26+
if (!token) return
27+
28+
try {
29+
const [
30+
newSubsResult,
31+
churnedSubsResult,
32+
issuedInvoicesResult,
33+
paidInvoicesResult,
34+
subsTimeSeriesResult
35+
] = await Promise.all([
36+
pipe(token, 'orb_new_subs'),
37+
pipe(token, 'orb_churn_subs'),
38+
pipe(token, 'orb_invoices_issued'),
39+
pipe(token, 'orb_invoices_paid'),
40+
pipe<{ data: SubsDataPoint[] }>(token, 'orb_subs_ts')
41+
])
42+
43+
setNewSubs(newSubsResult.data[0]?.subs || 0)
44+
setChurnedSubs(churnedSubsResult.data[0]?.subs || 0)
45+
setIssuedInvoices(issuedInvoicesResult.data[0]?.invoices || 0)
46+
setPaidInvoices(paidInvoicesResult.data[0]?.invoices || 0)
47+
setSubsTimeSeriesData(subsTimeSeriesResult.data)
48+
} catch (error) {
49+
console.error('Failed to fetch metrics:', error)
50+
}
51+
}
52+
53+
fetchMetrics()
54+
}, [token])
55+
56+
const churnRate = newSubs > 0
57+
? Math.round((churnedSubs / newSubs) * 100)
58+
: 0
59+
60+
const invoicePaymentRate = issuedInvoices > 0
61+
? Math.round((paidInvoices / issuedInvoices) * 100)
62+
: 0
63+
64+
return (
65+
<div className="space-y-8">
66+
<div>
67+
<h1 className="text-2xl font-bold">Orb Analytics</h1>
68+
<Link
69+
href={token ? `/?token=${token}` : '/'}
70+
className="text-sm text-muted-foreground hover:text-primary"
71+
>
72+
← Back to Apps
73+
</Link>
74+
</div>
75+
76+
{/* Metrics Row */}
77+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
78+
<MetricCard
79+
title="New Subscriptions"
80+
value={newSubs.toLocaleString()}
81+
description="New subscriptions in the last 30 days"
82+
/>
83+
<MetricCard
84+
title="Churned Subscriptions"
85+
value={churnedSubs.toLocaleString()}
86+
description="Cancelled subscriptions in the last 30 days"
87+
/>
88+
<MetricCard
89+
title="Churn Rate"
90+
value={`${churnRate}%`}
91+
description="Churned / New subscriptions"
92+
/>
93+
<MetricCard
94+
title="Invoice Payment Rate"
95+
value={`${invoicePaymentRate}%`}
96+
description="Paid / Issued invoices"
97+
/>
98+
</div>
99+
100+
{/* Charts Grid */}
101+
<div className="grid gap-4 md:grid-cols-2">
102+
<SubsChart data={subsTimeSeriesData} />
103+
<Card className="col-span-1">
104+
<CardHeader>
105+
<CardTitle>Revenue</CardTitle>
106+
</CardHeader>
107+
<CardContent className="h-[300px]">
108+
{/* Revenue chart will go here when we have the data */}
109+
</CardContent>
110+
</Card>
111+
</div>
112+
</div>
113+
)
114+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 SubsDataPoint {
18+
day: string
19+
invoices: number
20+
}
21+
22+
interface SubsChartProps {
23+
data: SubsDataPoint[]
24+
}
25+
26+
const chartConfig = {
27+
invoices: {
28+
color: "hsl(var(--primary))",
29+
label: "New Subscriptions",
30+
},
31+
} satisfies ChartConfig
32+
33+
export function SubsChart({ data }: SubsChartProps) {
34+
return (
35+
<Card>
36+
<CardHeader>
37+
<CardTitle>New Subscriptions</CardTitle>
38+
</CardHeader>
39+
<CardContent>
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 />}
59+
/>
60+
<Line
61+
type="monotone"
62+
dataKey="invoices"
63+
strokeWidth={2}
64+
activeDot={{
65+
r: 4,
66+
style: { fill: "hsl(var(--primary))" },
67+
}}
68+
style={{
69+
stroke: "hsl(var(--primary))",
70+
}}
71+
/>
72+
</LineChart>
73+
</ChartContainer>
74+
</CardContent>
75+
</Card>
76+
)
77+
}

0 commit comments

Comments
 (0)