Skip to content

Commit 4efd81e

Browse files
Improve: Usage Page (#181)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Adds fullscreen mode for usage metric charts, tweaks bar chart styling and time range label, removes initial prefix logic, and standardizes cost display formatting. > > - **Usage Charts UI**: > - **Fullscreen mode**: Adds hover "expand" button and dialog-based fullscreen view for `sandboxes`, `cost`, `vcpu`, `ram` charts via `UsageMetricChart` and context (`fullscreenMetric`, `setFullscreenMetric`). > - **Chart styling**: Adjusts bar appearance in `compute-usage-chart` (`borderWidth`, `opacity`, decal `symbolSize`, `dashArrayY`, `barCategoryGap`). > - **Timeframe & Display**: > - Removes initial data-point prefix logic; default range now always falls back to `INITIAL_TIMEFRAME_FALLBACK_RANGE_MS`. > - Updates time range label to show timezone on end date. > - **Formatting**: > - Uses `formatCurrency` for `cost` in hovered and total displays. > - **Components**: > - `DialogContent` supports `closeButtonClassName` and refines close button classes. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a5e27f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 224336a commit 4efd81e

File tree

7 files changed

+135
-39
lines changed

7 files changed

+135
-39
lines changed

src/features/dashboard/usage/compute-usage-chart/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,19 @@ function ComputeUsageChart({
141141
itemStyle: {
142142
color: 'transparent',
143143
borderColor: barColor,
144-
borderWidth: 0.3,
144+
borderWidth: 0.25,
145145
borderCap: 'square',
146-
opacity: 1,
146+
opacity: 0.8,
147147
decal: {
148148
symbol: 'line',
149-
symbolSize: 1.5,
149+
symbolSize: 0.8,
150150
rotation: -Math.PI / 4,
151151
dashArrayX: [1, 0],
152-
dashArrayY: [2, 4],
152+
dashArrayY: [1, 1.5],
153153
color: barColor,
154154
},
155155
},
156-
barCategoryGap: '15%',
156+
barCategoryGap: '28%',
157157
emphasis: {
158158
itemStyle: {
159159
opacity: 1,

src/features/dashboard/usage/constants.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@ import {
55
ComputeChartType,
66
} from './compute-usage-chart/types'
77

8-
/**
9-
* Initial timeframe prefix in milliseconds (3 days)
10-
* Used to set the default time range for displaying usage data
11-
*/
12-
export const INITIAL_TIMEFRAME_DATA_POINT_PREFIX_MS = 3 * 24 * 60 * 60 * 1000
13-
148
/**
159
* Default fallback range in milliseconds (30 days)
1610
* Used when no data is available to determine the appropriate range

src/features/dashboard/usage/display-utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
formatCurrency,
23
formatDateRange,
34
formatDay,
45
formatHour,
@@ -133,7 +134,7 @@ export function formatHoveredValues(
133134
timestamp: timestampLabel,
134135
},
135136
cost: {
136-
displayValue: `$${cost.toFixed(2)}`,
137+
displayValue: formatCurrency(cost),
137138
label,
138139
timestamp: timestampLabel,
139140
},
@@ -171,7 +172,7 @@ export function formatTotalValues(totals: {
171172
timestamp: null,
172173
},
173174
cost: {
174-
displayValue: `$${totals.cost.toFixed(2)}`,
175+
displayValue: formatCurrency(totals.cost),
175176
label: 'total over range',
176177
timestamp: null,
177178
},

src/features/dashboard/usage/usage-charts-context.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ import {
1111
useMemo,
1212
useState,
1313
} from 'react'
14-
import {
15-
INITIAL_TIMEFRAME_DATA_POINT_PREFIX_MS,
16-
INITIAL_TIMEFRAME_FALLBACK_RANGE_MS,
17-
} from './constants'
14+
import { INITIAL_TIMEFRAME_FALLBACK_RANGE_MS } from './constants'
1815
import {
1916
calculateTotals,
2017
formatAxisDate,
@@ -49,6 +46,10 @@ interface UsageChartsContextValue {
4946
vcpu: DisplayValue
5047
ram: DisplayValue
5148
}
49+
fullscreenMetric: 'sandboxes' | 'cost' | 'vcpu' | 'ram' | null
50+
setFullscreenMetric: (
51+
metric: 'sandboxes' | 'cost' | 'vcpu' | 'ram' | null
52+
) => void
5253
}
5354

5455
const UsageChartsContext = createContext<UsageChartsContextValue | undefined>(
@@ -77,20 +78,16 @@ export function UsageChartsProvider({
7778
})
7879

7980
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
81+
const [fullscreenMetric, setFullscreenMetric] = useState<
82+
'sandboxes' | 'cost' | 'vcpu' | 'ram' | null
83+
>(null)
8084

8185
// DERIVED STATE
8286

8387
const defaultRange = useMemo(() => {
8488
const now = Date.now()
85-
86-
if (data.hour_usages && data.hour_usages.length > 0) {
87-
const firstTimestamp = data.hour_usages[0]!.timestamp
88-
const start = firstTimestamp - INITIAL_TIMEFRAME_DATA_POINT_PREFIX_MS
89-
return { start, end: now }
90-
}
91-
9289
return { start: now - INITIAL_TIMEFRAME_FALLBACK_RANGE_MS, end: now }
93-
}, [data])
90+
}, [])
9491

9592
const timeframe = useMemo(
9693
() => ({
@@ -242,6 +239,8 @@ export function UsageChartsProvider({
242239
totals,
243240
samplingMode,
244241
displayValues,
242+
fullscreenMetric,
243+
setFullscreenMetric,
245244
}),
246245
[
247246
displayedData,
@@ -251,6 +250,7 @@ export function UsageChartsProvider({
251250
totals,
252251
samplingMode,
253252
displayValues,
253+
fullscreenMetric,
254254
]
255255
)
256256

src/features/dashboard/usage/usage-metric-chart.tsx

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
import { AnimatedMetricDisplay } from '@/features/dashboard/sandboxes/monitoring/charts/animated-metric-display'
44
import { cn } from '@/lib/utils'
5-
import { Card, CardContent, CardHeader } from '@/ui/primitives/card'
5+
import { Button } from '@/ui/primitives/button'
6+
import {
7+
Card,
8+
CardContent,
9+
CardHeader,
10+
cardVariants,
11+
} from '@/ui/primitives/card'
12+
import { Dialog, DialogContent } from '@/ui/primitives/dialog'
13+
import { DialogTitle } from '@radix-ui/react-dialog'
14+
import { Maximize2 } from 'lucide-react'
15+
import { useState } from 'react'
616
import ComputeUsageChart from './compute-usage-chart'
717
import { useUsageCharts } from './usage-charts-context'
818
import { UsageTimeRangeControls } from './usage-time-range-controls'
@@ -20,17 +30,17 @@ const METRIC_CONFIGS: Record<UsageMetricType, MetricConfig> = {
2030
ram: { title: 'RAM Hours' },
2131
}
2232

23-
interface UsageMetricChartProps {
33+
interface UsageMetricChartContentProps {
2434
metric: UsageMetricType
25-
className?: string
2635
timeRangeControlsClassName?: string
36+
isFullscreen?: boolean
2737
}
2838

29-
export function UsageMetricChart({
39+
function UsageMetricChartContent({
3040
metric,
31-
className,
3241
timeRangeControlsClassName,
33-
}: UsageMetricChartProps) {
42+
isFullscreen,
43+
}: UsageMetricChartContentProps) {
3444
const {
3545
displayedData,
3646
setHoveredIndex,
@@ -39,14 +49,17 @@ export function UsageMetricChart({
3949
displayValues,
4050
samplingMode,
4151
onBrushEnd,
52+
setFullscreenMetric,
4253
} = useUsageCharts()
4354

55+
const [isChartHovered, setIsChartHovered] = useState(false)
56+
4457
const config = METRIC_CONFIGS[metric]
4558
const { displayValue, label, timestamp } = displayValues[metric]
4659
const data = displayedData[metric]
4760

4861
return (
49-
<Card className={cn('h-full flex flex-col', className)}>
62+
<>
5063
<CardHeader className="space-y-2">
5164
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-2">
5265
<span className="prose-label-highlight uppercase max-md:text-sm">
@@ -55,7 +68,10 @@ export function UsageMetricChart({
5568
<UsageTimeRangeControls
5669
timeframe={timeframe}
5770
onTimeRangeChange={setTimeframe}
58-
className={cn('max-lg:self-start', timeRangeControlsClassName)}
71+
className={cn('max-lg:self-start', {
72+
[timeRangeControlsClassName ?? '']: !isFullscreen,
73+
'mr-8': isFullscreen,
74+
})}
5975
/>
6076
</div>
6177
<AnimatedMetricDisplay
@@ -65,7 +81,11 @@ export function UsageMetricChart({
6581
/>
6682
</CardHeader>
6783
<CardContent className="flex flex-col gap-4 flex-1 min-h-0">
68-
<div className="flex-1 min-h-0">
84+
<div
85+
className="flex-1 min-h-0 relative"
86+
onMouseEnter={() => setIsChartHovered(true)}
87+
onMouseLeave={() => setIsChartHovered(false)}
88+
>
6989
<ComputeUsageChart
7090
type={metric}
7191
data={data}
@@ -74,8 +94,76 @@ export function UsageMetricChart({
7494
onHoverEnd={() => setHoveredIndex(null)}
7595
onBrushEnd={onBrushEnd}
7696
/>
97+
{isChartHovered && !isFullscreen && (
98+
<Button
99+
onClick={() => setFullscreenMetric(metric)}
100+
variant="ghost"
101+
size="iconSm"
102+
className={cn(
103+
cardVariants({ variant: 'layer' }),
104+
'hidden lg:flex absolute top-4 right-4 opacity-70 hover:opacity-100 animate-fade-slide-in'
105+
)}
106+
aria-label="Expand chart to fullscreen"
107+
>
108+
<Maximize2 className="size-4" />
109+
</Button>
110+
)}
77111
</div>
78112
</CardContent>
79-
</Card>
113+
</>
114+
)
115+
}
116+
117+
interface UsageMetricChartProps {
118+
metric: UsageMetricType
119+
timeRangeControlsClassName?: string
120+
className?: string
121+
}
122+
123+
export function UsageMetricChart({
124+
metric,
125+
className,
126+
timeRangeControlsClassName,
127+
}: UsageMetricChartProps) {
128+
const { fullscreenMetric, setFullscreenMetric } = useUsageCharts()
129+
130+
const isFullscreen = fullscreenMetric === metric
131+
132+
return (
133+
<>
134+
{!isFullscreen && (
135+
<Card className={cn('h-full flex flex-col', className)}>
136+
<UsageMetricChartContent
137+
metric={metric}
138+
timeRangeControlsClassName={timeRangeControlsClassName}
139+
isFullscreen={isFullscreen}
140+
/>
141+
</Card>
142+
)}
143+
144+
{isFullscreen && (
145+
<Dialog
146+
open={isFullscreen}
147+
onOpenChange={(open) => !open && setFullscreenMetric(null)}
148+
>
149+
<DialogContent
150+
className="sm:max-w-[min(90svw,2200px)] w-full max-h-[min(70svh,1200px)] h-full border-0 p-0"
151+
closeButtonClassName="top-7.5 right-6.5"
152+
>
153+
{/* title just here to avoid accessibility dev error from radix */}
154+
<DialogTitle className="sr-only">
155+
{METRIC_CONFIGS[metric].title}
156+
</DialogTitle>
157+
<Card className="h-full flex flex-col border-0">
158+
<UsageMetricChartContent
159+
metric={metric}
160+
timeRangeControlsClassName={timeRangeControlsClassName}
161+
isFullscreen={isFullscreen}
162+
/>
163+
</Card>
164+
</DialogContent>
165+
</Dialog>
166+
)}
167+
</>
80168
)
81169
}

src/features/dashboard/usage/usage-time-range-controls.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
normalizeToEndOfSamplingPeriod,
2222
normalizeToStartOfSamplingPeriod,
2323
} from './sampling-utils'
24-
import { useUsageCharts } from './usage-charts-context'
2524

2625
interface UsageTimeRangeControlsProps {
2726
timeframe: {
@@ -51,12 +50,19 @@ export function UsageTimeRangeControls({
5150
)
5251

5352
const rangeLabel = useMemo(() => {
54-
const formatter = new Intl.DateTimeFormat('en-US', {
53+
const opt: Intl.DateTimeFormatOptions = {
5554
year: 'numeric',
5655
month: 'short',
5756
day: 'numeric',
57+
}
58+
59+
const firstFormatter = new Intl.DateTimeFormat('en-US', opt)
60+
61+
const lastFormatter = new Intl.DateTimeFormat('en-US', {
62+
...opt,
63+
timeZoneName: 'short',
5864
})
59-
return `${formatter.format(timeframe.start)} - ${formatter.format(timeframe.end)}`
65+
return `${firstFormatter.format(timeframe.start)} - ${lastFormatter.format(timeframe.end)}`
6066
}, [timeframe.start, timeframe.end])
6167

6268
const rangeCopyValue = useMemo(

src/ui/primitives/dialog.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ function DialogOverlay({
4949
interface DialogContentProps
5050
extends React.ComponentProps<typeof DialogPrimitive.Content> {
5151
hideClose?: boolean
52+
closeButtonClassName?: string
5253
}
5354

5455
function DialogContent({
5556
className,
5657
children,
5758
hideClose,
59+
closeButtonClassName,
5860
...props
5961
}: DialogContentProps) {
6062
return (
@@ -71,7 +73,12 @@ function DialogContent({
7173
>
7274
{children}
7375
{!hideClose && (
74-
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg- data-[state=open]:text-muted-foreground absolute top-4 right-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
76+
<DialogPrimitive.Close
77+
className={cn(
78+
`ring-offset-bg cursor-pointer focus:ring-ring data-[state=open]:text-fg-tertiary absolute top-4 right-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
79+
closeButtonClassName
80+
)}
81+
>
7582
<XIcon />
7683
<span className="sr-only">Close</span>
7784
</DialogPrimitive.Close>

0 commit comments

Comments
 (0)