diff --git a/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx b/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx index dc2fcc7524f..92d3c967c3c 100644 --- a/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx +++ b/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx @@ -1,5 +1,12 @@ "use client"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { type ChartConfig, ChartContainer, @@ -15,8 +22,14 @@ import { EmptyChartState, LoadingChartState, } from "../../../../components/analytics/empty-chart-state"; +import { cn } from "../../../lib/utils"; type ThirdwebAreaChartProps = { + header?: { + title: string; + description?: string; + titleClassName?: string; + }; // chart config config: TConfig; data: Array & { time: number | string | Date }>; @@ -25,78 +38,99 @@ type ThirdwebAreaChartProps = { // chart className chartClassName?: string; isPending: boolean; + hideLabel?: boolean; + toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode; }; export function ThirdwebAreaChart( props: ThirdwebAreaChartProps, ) { const configKeys = useMemo(() => Object.keys(props.config), [props.config]); + return ( -
- - {props.isPending ? ( - - ) : props.data.length === 0 ? ( - - ) : ( - - - formatDate(new Date(value), "MMM dd")} - /> - } /> - + + {props.header && ( + + + {props.header.title} + + {props.header.description && ( + {props.header.description} + )} + + )} + + + + {props.isPending ? ( + + ) : props.data.length === 0 ? ( + + ) : ( + + + formatDate(new Date(value), "MMM dd")} + /> + + } + /> + + {configKeys.map((key) => ( + + + + + ))} + {configKeys.map((key) => ( - - - - + dataKey={key} + type="natural" + fill={`url(#fill_${key})`} + fillOpacity={0.4} + stroke={`var(--color-${key})`} + stackId="a" + /> ))} - - {configKeys.map((key) => ( - - ))} - {props.showLegend && ( - } className="pt-8" /> - )} - - )} - -
+ {props.showLegend && ( + } + /> + )} + + )} + + + ); } diff --git a/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx b/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx index 42de009738b..7291a8e45fc 100644 --- a/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx +++ b/apps/dashboard/src/@/components/blocks/charts/bar-chart.tsx @@ -27,8 +27,11 @@ import { cn } from "../../../lib/utils"; type ThirdwebBarChartProps = { // metadata - title: string; - description?: string; + header?: { + title: string; + description?: string; + titleClassName?: string; + }; // chart config config: TConfig; data: Array & { time: number | string | Date }>; @@ -39,7 +42,6 @@ type ThirdwebBarChartProps = { isPending: boolean; toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode; hideLabel?: boolean; - titleClassName?: string; }; export function ThirdwebBarChart( @@ -52,15 +54,18 @@ export function ThirdwebBarChart( return ( - - - {props.title} - - {props.description && ( - {props.description} - )} - - + {props.header && ( + + + {props.header.title} + + {props.header.description && ( + {props.header.description} + )} + + )} + + {props.isPending ? ( @@ -87,7 +92,9 @@ export function ThirdwebBarChart( } /> {props.showLegend && ( - } /> + } + /> )} {configKeys.map((key, idx) => ( = ({ const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => { return ( - + {label} {0} @@ -350,7 +364,7 @@ const AnalyticsData: React.FC = ({ }, [totalQuery.data]); return ( - + {label} {data.toLocaleString()} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx index 67a61bdc566..a6e9ba4b612 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/Analytics.tsx @@ -135,7 +135,7 @@ function OverviewAnalytics(props: ChartProps) { data={mergedData || []} isPending={isPending} showLegend - chartClassName="aspect-[1.5] lg:aspect-[4.5]" + chartClassName="aspect-[1.5] lg:aspect-[3.5]" /> ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx index 493eabfb013..725a0cd2d3f 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx @@ -532,13 +532,15 @@ export function TransactionCharts(props: { return ( +
+
+
+

+ Nebula +

+
+ +
+
+ +
+ } + > + + +
+ + ); +} + +async function NebulaAnalyticDashboard(props: { + accountId: string; + authToken: string; + searchParams: { + from: string | undefined | string[]; + to: string | undefined | string[]; + interval: string | undefined | string[]; + }; +}) { + const { range, interval } = getNebulaAnalyticsRangeFromSearchParams( + props.searchParams, + ); + + const res = await fetchNebulaAnalytics({ + accountId: props.accountId, + authToken: props.authToken, + from: normalizeTimeISOString(range.from), + to: normalizeTimeISOString(range.to), + interval, + }); + + if (!res.ok) { + return ( +
+
+

+ Failed to fetch Nebula analytics +

+

{res.error}

+
+
+ ); + } + + return ; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx index 16a61969bf9..ab791a3cab4 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx @@ -37,9 +37,6 @@ function Story() { return (
-

- Story Variants -

({ date: subDays(new Date(), i).toISOString(), - totalPromptTokens: randomInt(1000), - totalCompletionTokens: randomInt(1000), - totalSessions: randomInt(100), - totalRequests: randomInt(4000), + totalPromptTokens: randomInt(500, 700 + i * 100), + totalCompletionTokens: randomInt(1000, 2000 + i * 100), + totalSessions: randomInt(400, 1000 + i * 100), + totalRequests: randomInt(4000, 5000 + i * 100), })); } -function randomInt(max: number) { - return Math.floor(Math.random() * max); +function randomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.tsx index 78bdb8cf9db..944390db928 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.tsx @@ -1,5 +1,6 @@ -import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart"; +import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart"; import { SkeletonContainer } from "@/components/ui/skeleton"; +import { formatDate } from "date-fns"; import { ActivityIcon, MessageCircleQuestionIcon, @@ -7,17 +8,7 @@ import { MessageSquareQuoteIcon, } from "lucide-react"; import { useMemo } from "react"; -import { - ResponsiveSearchParamsProvider, - ResponsiveSuspense, -} from "responsive-rsc"; -import { normalizeTimeISOString } from "../../../../../../../lib/time"; -import { - type NebulaAnalyticsDataItem, - fetchNebulaAnalytics, -} from "./fetch-nebula-analytics"; -import { NebulaAnalyticsFilter } from "./nebula-analytics-filter"; -import { getNebulaAnalyticsRangeFromSearchParams } from "./utils"; +import type { NebulaAnalyticsDataItem } from "./fetch-nebula-analytics"; type ChartData = { time: Date; @@ -27,79 +18,46 @@ type ChartData = { totalRequests: number; }; -export function NebulaAnalyticsPage(props: { - searchParams: { - from: string | undefined | string[]; - to: string | undefined | string[]; - interval: string | undefined | string[]; - }; - accountId: string; - authToken: string; -}) { - return ( - -
-
-
-

- Nebula -

-
- -
-
- -
- } - > - - -
-
- ); -} +type AnalyticsChartProps = { + data: ChartData[]; + isPending: boolean; + title: string; + description: string; + dataKey: keyof ChartData; + color: string; +}; -async function NebulaAnalyticDashboard(props: { - accountId: string; - authToken: string; - searchParams: { - from: string | undefined | string[]; - to: string | undefined | string[]; - interval: string | undefined | string[]; - }; -}) { - const { range, interval } = getNebulaAnalyticsRangeFromSearchParams( - props.searchParams, +function AnalyticsChart({ + data, + isPending, + title, + description, + dataKey, + color, +}: AnalyticsChartProps) { + return ( + ({ + ...item, + time: item.time.getTime(), + }))} + isPending={isPending} + header={{ + title, + description, + titleClassName: "text-xl mb-1", + }} + chartClassName="aspect-[1.5] lg:aspect-[2.5]" + hideLabel={false} + toolTipLabelFormatter={toolTipLabelFormatter} + config={{ + [dataKey]: { + label: title, + color, + }, + }} + /> ); - - const res = await fetchNebulaAnalytics({ - accountId: props.accountId, - authToken: props.authToken, - from: normalizeTimeISOString(range.from), - to: normalizeTimeISOString(range.to), - interval, - }); - - if (!res.ok) { - return ( -
-
-

- Failed to fetch Nebula analytics -

-

{res.error}

-
-
- ); - } - - return ; } export function NebulaAnalyticsDashboardUI(props: { @@ -142,94 +100,82 @@ export function NebulaAnalyticsDashboardUI(props: {
-
+
-
- + - - -
); } +function toolTipLabelFormatter(_v: string, item: unknown) { + if (Array.isArray(item)) { + const time = item[0].payload.time as number; + return formatDate(new Date(time), "MMM d, yyyy"); + } + return undefined; +} + function StatCard(props: { title: string; value: number; @@ -238,7 +184,7 @@ function StatCard(props: { }) { return (
-
+

{props.title}

@@ -247,9 +193,7 @@ function StatCard(props: { skeletonData={10000} loadedData={props.isPending ? undefined : props.value} render={(v) => ( -

- {v} -

+

{v}

)} />