-
Notifications
You must be signed in to change notification settings - Fork 895
feat(admin): enhance analytics dashboard with improved charts and UX #488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e1405fe
9ccf96e
88dca14
d8f1a5a
21fb8ed
b31e7d4
b7e7207
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import Image from "next/image"; | ||||||||||||||||||||||||||||||
| import { useEffect, useState } from "react"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const DEMO_DAY = new Date("2026-06-16T00:00:00"); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function calculateDaysLeft(): number { | ||||||||||||||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||||||||||||||
| const diff = DEMO_DAY.getTime() - now.getTime(); | ||||||||||||||||||||||||||||||
| if (diff <= 0) return 0; | ||||||||||||||||||||||||||||||
| return diff / (1000 * 60 * 60 * 24); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export function DemoCountdown() { | ||||||||||||||||||||||||||||||
| const [daysLeft, setDaysLeft] = useState<number>(calculateDaysLeft); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| const timer = setInterval(() => { | ||||||||||||||||||||||||||||||
| setDaysLeft(calculateDaysLeft()); | ||||||||||||||||||||||||||||||
| }, 100); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return () => clearInterval(timer); | ||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excessive update frequency wastes CPU and battery. The 100ms interval (line 20) updates the countdown 10 times per second, which is unnecessary for a day counter. Even with 6 decimal places, updating every second (1000ms) would provide smooth animation while being 10× more efficient. This impacts performance, especially on mobile devices and battery-powered laptops. Recommended fix: Change to 1 second interval useEffect(() => {
const timer = setInterval(() => {
setDaysLeft(calculateDaysLeft());
- }, 100);
+ }, 1000); // Update once per second
return () => clearInterval(timer);
}, []);Alternatively, consider reducing decimal precision to 2-3 places and updating less frequently: - const fraction = (daysLeft - wholeDays).toFixed(6).slice(1); // ".466739"
+ const fraction = (daysLeft - wholeDays).toFixed(2).slice(1); // ".47"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const wholeDays = Math.floor(daysLeft); | ||||||||||||||||||||||||||||||
| const fraction = (daysLeft - wholeDays).toFixed(6).slice(1); // ".466739" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <div className="flex items-center justify-end gap-3"> | ||||||||||||||||||||||||||||||
| <Image src="/yc-logo.png" alt="Y Combinator" width={40} height={40} /> | ||||||||||||||||||||||||||||||
| <div className="flex items-baseline font-mono" suppressHydrationWarning> | ||||||||||||||||||||||||||||||
| <span className="text-4xl font-bold">{wholeDays}</span> | ||||||||||||||||||||||||||||||
| <span className="text-xl text-muted-foreground">{fraction}</span> | ||||||||||||||||||||||||||||||
| <span className="ml-1 text-xl font-sans text-muted-foreground"> | ||||||||||||||||||||||||||||||
| days | ||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { DemoCountdown } from "./DemoCountdown"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| "use client"; | ||
|
|
||
| import { | ||
| Card, | ||
| CardContent, | ||
| CardDescription, | ||
| CardHeader, | ||
| CardTitle, | ||
| } from "@superset/ui/card"; | ||
| import { | ||
| type ChartConfig, | ||
| ChartContainer, | ||
| ChartTooltip, | ||
| ChartTooltipContent, | ||
| } from "@superset/ui/chart"; | ||
| import { Skeleton } from "@superset/ui/skeleton"; | ||
| import type { ReactNode } from "react"; | ||
| import { Bar, BarChart, XAxis, YAxis } from "recharts"; | ||
|
|
||
| interface FunnelStep { | ||
| name: string; | ||
| count: number; | ||
| conversionRate: number; | ||
| } | ||
|
|
||
| interface FunnelChartProps { | ||
| title: string; | ||
| description?: string; | ||
| data: FunnelStep[] | null | undefined; | ||
| isLoading?: boolean; | ||
| error?: { message: string } | null; | ||
| headerAction?: ReactNode; | ||
| } | ||
|
|
||
| const chartConfig = { | ||
| count: { | ||
| label: "Users", | ||
| color: "var(--chart-1)", | ||
| }, | ||
| } satisfies ChartConfig; | ||
|
|
||
| export function FunnelChart({ | ||
| title, | ||
| description, | ||
| data, | ||
| isLoading, | ||
| error, | ||
| headerAction, | ||
| }: FunnelChartProps) { | ||
| return ( | ||
| <Card> | ||
| <CardHeader> | ||
| <div className="flex items-center justify-between"> | ||
| <CardTitle>{title}</CardTitle> | ||
| {headerAction} | ||
| </div> | ||
| {description && <CardDescription>{description}</CardDescription>} | ||
| </CardHeader> | ||
| <CardContent> | ||
| {isLoading ? ( | ||
| <div className="space-y-3"> | ||
| <Skeleton className="h-6 w-full" /> | ||
| <Skeleton className="h-6 w-4/5" /> | ||
| <Skeleton className="h-6 w-3/5" /> | ||
| <Skeleton className="h-6 w-2/5" /> | ||
| </div> | ||
| ) : error ? ( | ||
| <div className="flex h-[200px] items-center justify-center"> | ||
| <p className="text-destructive text-sm"> | ||
| Failed to load funnel data | ||
| </p> | ||
| </div> | ||
| ) : !data || data.length === 0 ? ( | ||
| <div className="flex h-[200px] items-center justify-center rounded-md border border-dashed"> | ||
| <p className="text-muted-foreground text-sm"> | ||
| No funnel data available for this period | ||
| </p> | ||
| </div> | ||
| ) : ( | ||
| <ChartContainer config={chartConfig} className="h-[200px] w-full"> | ||
| <BarChart | ||
| data={data} | ||
| layout="vertical" | ||
| margin={{ left: 0, right: 40 }} | ||
| > | ||
| <XAxis type="number" hide /> | ||
| <YAxis | ||
| type="category" | ||
| dataKey="name" | ||
| tickLine={false} | ||
| axisLine={false} | ||
| width={120} | ||
| tick={{ fontSize: 12 }} | ||
| /> | ||
| <ChartTooltip | ||
| cursor={false} | ||
| content={ | ||
| <ChartTooltipContent | ||
| formatter={(value, _name, item) => ( | ||
| <div className="flex flex-col gap-1"> | ||
| <span>{value.toLocaleString()} users</span> | ||
| <span className="text-muted-foreground"> | ||
| {item.payload.conversionRate.toFixed(1)}% conversion | ||
| </span> | ||
| </div> | ||
| )} | ||
| /> | ||
| } | ||
| /> | ||
| <Bar | ||
| dataKey="count" | ||
| fill="var(--color-count)" | ||
| radius={[0, 4, 4, 0]} | ||
| label={{ | ||
| position: "right", | ||
| fontSize: 12, | ||
| formatter: (value: number) => value.toLocaleString(), | ||
| }} | ||
| /> | ||
| </BarChart> | ||
| </ChartContainer> | ||
| )} | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { FunnelChart } from "./FunnelChart"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 2415
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 91
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 91
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 112
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 1430
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 593
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 91
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 3158
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 2478
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 2650
Ensure
POSTHOG_API_KEYandPOSTHOG_PROJECT_IDsecrets are configured in GitHub repository settings.These server-side credentials are required by the tRPC package for authenticated requests to PostHog's analytics API. Without these secrets configured, the deployment will fail during the build phase when the tRPC environment validation runs, or the PostHog integration will not function in production.
🤖 Prompt for AI Agents