Skip to content

Enhanced Usage Dashboard and Backend Refinements #58

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 20 additions & 28 deletions src/app/dashboard/[teamIdOrSlug]/usage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import DashboardPageLayout from '@/features/dashboard/page-layout'
import { CostCard } from '@/features/dashboard/usage/cost-card'
import { RAMCard } from '@/features/dashboard/usage/ram-card'
import { SandboxesCard } from '@/features/dashboard/usage/sandboxes-card'
import { VCPUCard } from '@/features/dashboard/usage/vcpu-card'
import { resolveTeamIdInServerComponent } from '@/lib/utils/server'
import { getUsage } from '@/server/usage/get-usage'
import { AssemblyLoader } from '@/ui/loader'
import { Suspense } from 'react'
import { CatchErrorBoundary } from '@/ui/error'

export default async function UsagePage({
params,
Expand All @@ -18,45 +17,38 @@ export default async function UsagePage({
return (
<DashboardPageLayout
title="Usage"
className="relative grid max-h-full min-h-[calc(360px+320px)] w-full grid-cols-1 self-start lg:grid-cols-12"
className="relative grid max-h-full w-full grid-cols-1 self-start lg:grid-cols-12"
>
<Suspense
fallback={
<div className="absolute top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center gap-3">
<AssemblyLoader gridWidth={7} gridHeight={3} />
<h2 className="text-fg-500 text-lg font-medium">Collecting data</h2>
</div>
}
>
<UsagePageContent teamId={teamId} />
</Suspense>
<SandboxesCard
teamId={teamId}
className="col-span-1 min-h-[360px] border-b lg:col-span-12"
/>
<UsagePageContent teamId={teamId} />
</DashboardPageLayout>
)
}

async function UsagePageContent({ teamId }: { teamId: string }) {
const res = await getUsage({ teamId })

if (!res?.data || res.serverError || res.validationErrors) {
throw new Error(res?.serverError || 'Failed to load usage')
}

const data = res.data

function UsagePageContent({ teamId }: { teamId: string }) {
return (
<>
<CatchErrorBoundary
hideFrame
classNames={{
wrapper: 'col-span-full bg-bg',
errorBoundary: 'mx-auto',
}}
>
<CostCard
data={data}
teamId={teamId}
className="col-span-1 min-h-[360px] border-b lg:col-span-12"
/>
<VCPUCard
data={data}
teamId={teamId}
className="col-span-1 min-h-[320px] border-b lg:col-span-6 lg:border-r lg:border-b-0"
/>
<RAMCard
data={data}
teamId={teamId}
className="col-span-1 min-h-[320px] border-b lg:col-span-6 lg:border-b-0"
/>
</>
</CatchErrorBoundary>
)
}
1 change: 0 additions & 1 deletion src/configs/dashboard-navs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export const MAIN_DASHBOARD_LINKS: DashboardNavLink[] = [
},
]
: []),

{
label: 'Team',
href: (args) => `/dashboard/${args.teamIdOrSlug}/team`,
Expand Down
6 changes: 4 additions & 2 deletions src/features/dashboard/billing/credits-content.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { getUsage } from '@/server/usage/get-usage'
import { getUsageThroughReactCache } from '@/server/usage/get-usage'

export default async function BillingCreditsContent({
teamId,
}: {
teamId: string
}) {
const res = await getUsage({ teamId })
const res = await getUsageThroughReactCache({
teamId,
})

if (!res?.data || res.serverError) {
throw new Error(res?.serverError || 'Failed to load credits')
Expand Down
65 changes: 49 additions & 16 deletions src/features/dashboard/usage/cost-card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import {
Card,
CardContent,
Expand All @@ -6,17 +7,50 @@ import {
CardDescription,
} from '@/ui/primitives/card'
import { CostChart } from './cost-chart'
import { TransformedUsageData } from '@/server/usage/types'
import { ChartPlaceholder } from '@/ui/chart-placeholder'
import { getUsageThroughReactCache } from '@/server/usage/get-usage'

async function CostCardContentResolver({ teamId }: { teamId: string }) {
const result = await getUsageThroughReactCache({ teamId })

if (!result?.data || result.serverError || result.validationErrors) {
const errorMessage =
result?.serverError ||
(Array.isArray(result?.validationErrors?.formErrors) &&
result?.validationErrors?.formErrors[0]) ||
'Could not load cost usage data.'
console.error(`CostCard Error: ${errorMessage}`, result)
throw new Error(errorMessage)
}

const dataFromAction = result.data

const latestCost = dataFromAction.costSeries?.[0]?.data?.at(-1)?.y

return (
<>
<div className="flex items-baseline gap-2">
<p className="font-mono text-2xl">
$
{new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(latestCost || 0)}
</p>
<span className="text-fg-500 text-xs">this month</span>
</div>
<CostChart data={dataFromAction.costSeries?.[0]?.data || []} />
</>
)
}

export function CostCard({
data,
teamId,
className,
}: {
data: TransformedUsageData
teamId: string
className?: string
}) {
const latestCost = data.costSeries[0].data.at(-1)?.y

return (
<Card className={className}>
<CardHeader>
Expand All @@ -26,17 +60,16 @@ export function CostCard({
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex items-baseline gap-2">
<p className="font-mono text-2xl">
$
{new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(latestCost || 0)}
</p>
<span className="text-fg-500 text-xs">this month</span>
</div>
<CostChart data={data.costSeries[0].data} />
<Suspense
fallback={
<ChartPlaceholder
isLoading={true}
classNames={{ container: 'h-48' }}
/>
}
>
<CostCardContentResolver teamId={teamId} />
</Suspense>
</CardContent>
</Card>
)
Expand Down
63 changes: 48 additions & 15 deletions src/features/dashboard/usage/ram-card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import {
Card,
CardContent,
Expand All @@ -6,17 +7,49 @@ import {
CardDescription,
} from '@/ui/primitives/card'
import { RAMChart } from './ram-chart'
import { TransformedUsageData } from '@/server/usage/types'
import { ChartPlaceholder } from '@/ui/chart-placeholder'
import { getUsageThroughReactCache } from '@/server/usage/get-usage'

async function RAMCardContentResolver({ teamId }: { teamId: string }) {
const result = await getUsageThroughReactCache({ teamId })

if (!result?.data || result.serverError || result.validationErrors) {
const errorMessage =
result?.serverError ||
(Array.isArray(result?.validationErrors?.formErrors) &&
result?.validationErrors?.formErrors[0]) ||
'Could not load RAM usage data.'
console.error(`RAMCard Error: ${errorMessage}`, result)
throw new Error(errorMessage)
}

const dataFromAction = result.data

const latestRAM = dataFromAction.ramSeries?.[0]?.data?.at(-1)?.y

return (
<>
<div className="flex items-baseline gap-2">
<p className="font-mono text-2xl">
{new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(latestRAM || 0)}
</p>
<span className="text-fg-500 text-xs">GB-hours this month</span>
</div>
<RAMChart data={dataFromAction.ramSeries?.[0]?.data || []} />
</>
)
}

export function RAMCard({
data,
teamId,
className,
}: {
data: TransformedUsageData
teamId: string
className?: string
}) {
const latestRAM = data.ramSeries[0].data.at(-1)?.y

return (
<Card className={className}>
<CardHeader>
Expand All @@ -26,16 +59,16 @@ export function RAMCard({
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex items-baseline gap-2">
<p className="font-mono text-2xl">
{new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(latestRAM || 0)}
</p>
<span className="text-fg-500 text-xs">GB-hours this month</span>
</div>
<RAMChart data={data.ramSeries[0].data} />
<Suspense
fallback={
<ChartPlaceholder
isLoading={true}
classNames={{ container: 'h-48' }}
/>
}
>
<RAMCardContentResolver teamId={teamId} />
</Suspense>
</CardContent>
</Card>
)
Expand Down
74 changes: 74 additions & 0 deletions src/features/dashboard/usage/sandboxes-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getSandboxesStarted } from '@/server/usage/get-sandboxes-started'
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from '@/ui/primitives/card'
import { Suspense } from 'react'
import { SandboxesChart } from './sandboxes-chart'
import { ChartPlaceholder } from '@/ui/chart-placeholder'

export function SandboxesCard({
className,
teamId,
}: {
className?: string
teamId: string
}) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="font-mono">Sandboxes Started</CardTitle>
<CardDescription>
The number of sandboxes your team started.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Suspense
fallback={
<ChartPlaceholder
key="chart-placeholder-sandboxes"
isLoading={true}
classNames={{ container: 'h-60' }}
/>
}
>
<SandboxesStartedContent teamId={teamId} />
</Suspense>
</CardContent>
</Card>
)
}

async function SandboxesStartedContent({ teamId }: { teamId: string }) {
const response = await getSandboxesStarted({ teamId })

if (response?.serverError || response?.validationErrors || !response?.data) {
throw new Error(response?.serverError || 'Failed to load usage')
}

// This rerenders the chart placeholder, which makes it look weird when it transitions from loading to empty in some cases.
// TODO: Fix this.
if (response.data.sandboxesStarted.length === 0) {
return (
<ChartPlaceholder
key="chart-placeholder-sandboxes"
emptyContent={<p>No started sandbox data found.</p>}
classNames={{
container: 'h-60',
}}
/>
)
}

return (
<SandboxesChart
data={response.data.sandboxesStarted}
classNames={{
container: 'h-60',
}}
/>
)
}
Loading