Changed the single scope icon from CircleDot to Square, which visually complements the Layers icon better. Both icons now share a similar geometric, rectangular aesthetic - a single square for single scope versus stacked squares/rectangles for multiple scope.
Here are all the files you need for the alerts tab:
app/page.tsximport { AlertsDashboard } from "@/components/alerts-dashboard"
export default function Page() {
return <AlertsDashboard />
}lib/alerts-data.tsexport type AlertType = "spend" | "credit_balance" | "usage"
export type AlertScope = "multiple" | "single"
export type SpendGroupBy = "pricing_plan_subscription" | "cadence"
export type UsageGroupBy = "pricing_plan_subscription" | "customer"
export type CreditBalanceGroupBy = "customer"
export type AlertGroupBy =
| SpendGroupBy
| UsageGroupBy
| CreditBalanceGroupBy
export const GROUP_BY_LABELS: Record<AlertGroupBy, string> = {
pricing_plan_subscription: "Pricing Plan Subscription",
cadence: "Cadence",
customer: "Customer",
}
export type SpendAggregationPeriod = "billing_period" | "service_period"
export type UsageAggregationPeriod = "service_period" | "all_time"
export type AlertAggregationPeriod =
| SpendAggregationPeriod
| UsageAggregationPeriod
export const AGGREGATION_PERIOD_LABELS: Record<AlertAggregationPeriod, string> = {
billing_period: "Billing Period",
service_period: "Service Period",
all_time: "All-Time",
}
export interface Alert {
id: string
name: string
active: boolean
type: AlertType
meter: string | null
threshold: string
conditions: string
scope: AlertScope
groupBy: AlertGroupBy
aggregationPeriod: AlertAggregationPeriod | null
lastFired: string | null
customerId: string | null
customerName: string | null
pricingPlan: string | null
subscriptionId: string | null
cadenceId: string | null
createdAt: string
}
export const ALERT_TYPE_LABELS: Record<AlertType, string> = {
spend: "Spend",
credit_balance: "Credit Balance",
usage: "Usage",
}
export const GROUP_BY_OPTIONS_BY_TYPE: Record<AlertType, AlertGroupBy[]> = {
spend: ["pricing_plan_subscription", "cadence"],
usage: ["pricing_plan_subscription", "customer"],
credit_balance: ["customer"],
}
export const AGGREGATION_PERIOD_OPTIONS_BY_TYPE: Record<
AlertType,
AlertAggregationPeriod[] | null
> = {
spend: ["billing_period", "service_period"],
usage: ["service_period", "all_time"],
credit_balance: null,
}
export const MOCK_ALERTS: Alert[] = [
// Add your mock data here - see the full file for all alerts
{
id: "alt_01HZQ3K8VX",
name: "Monthly Spend Cap",
active: true,
type: "spend",
meter: null,
threshold: "$5,000.00 USD",
conditions: '"Pro" Pricing Plan',
scope: "multiple",
groupBy: "pricing_plan_subscription",
aggregationPeriod: "billing_period",
lastFired: "Feb 7, 2026, 2:15 PM",
customerId: null,
customerName: null,
pricingPlan: "Pro",
subscriptionId: null,
cadenceId: null,
createdAt: "Jan 15, 2026",
},
// ... more alerts
]
export type ScopeFilterField = "customerId" | "subscriptionId" | "cadenceId"
export const SCOPE_FILTER_LABELS: Record<ScopeFilterField, string> = {
customerId: "Customer",
subscriptionId: "Subscription",
cadenceId: "Cadence",
}
export const SCOPE_FILTERS_BY_TYPE: Record<AlertType, ScopeFilterField[]> = {
spend: ["subscriptionId", "cadenceId"],
usage: ["subscriptionId", "customerId"],
credit_balance: ["customerId"],
}
export const ALL_SCOPE_FILTERS: ScopeFilterField[] = [
"customerId",
"subscriptionId",
"cadenceId",
]
export function getUniqueMeters(
alerts: Alert[],
): { id: string; label: string }[] {
const seen = new Set<string>()
for (const a of alerts) {
if (a.type === "usage" && a.meter && !seen.has(a.meter)) {
seen.add(a.meter)
}
}
return Array.from(seen)
.sort()
.map((m) => ({ id: m, label: m }))
}
export function getUniqueValues(
alerts: Alert[],
field: ScopeFilterField,
): { id: string; label: string }[] {
const seen = new Map<string, string>()
for (const a of alerts) {
const val = a[field]
if (val && !seen.has(val)) {
let label = val
if (field === "customerId" && a.customerName) {
label = `${a.customerName} (${val})`
}
seen.set(val, label)
}
}
return Array.from(seen.entries())
.map(([id, label]) => ({ id, label }))
.sort((a, b) => a.label.localeCompare(b.label))
}components/alerts-dashboard.tsx"use client"
import { useState, useMemo, useCallback } from "react"
import type { AlertType, Alert, ScopeFilterField } from "@/lib/alerts-data"
import { MOCK_ALERTS } from "@/lib/alerts-data"
import { DashboardHeader } from "@/components/dashboard-header"
import { AlertsFilterBar } from "@/components/alerts-filter-bar"
import type { ScopeAttributeFilters } from "@/components/alerts-filter-bar"
import { AlertsTable } from "@/components/alerts-table"
import { AlertDetailPanel } from "@/components/alert-detail-panel"
import { AppSidebar } from "@/components/app-sidebar"
import { SidebarProvider } from "@/components/ui/sidebar"
const EMPTY_SCOPE_ATTRIBUTES: ScopeAttributeFilters = {
customerId: null,
subscriptionId: null,
cadenceId: null,
}
export function AlertsDashboard() {
const [selectedType, setSelectedType] = useState<AlertType | "all">("all")
const [scopeFilter, setScopeFilter] = useState<"all" | "multiple" | "single">("all")
const [activeFilter, setActiveFilter] = useState<"all" | "active" | "inactive">("all")
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null)
const [meterFilter, setMeterFilter] = useState<string | null>(null)
const [scopeAttributeFilters, setScopeAttributeFilters] =
useState<ScopeAttributeFilters>({ ...EMPTY_SCOPE_ATTRIBUTES })
const handleTypeChange = useCallback((type: AlertType | "all") => {
setSelectedType(type)
if (type !== "usage" && type !== "all") {
setMeterFilter(null)
}
}, [])
const handleScopeChange = useCallback((scope: "all" | "multiple" | "single") => {
setScopeFilter(scope)
if (scope !== "single") {
setScopeAttributeFilters({ ...EMPTY_SCOPE_ATTRIBUTES })
}
}, [])
const handleScopeAttributeChange = useCallback(
(field: ScopeFilterField, value: string | null) => {
setScopeAttributeFilters((prev) => ({ ...prev, [field]: value }))
},
[]
)
const handleClearScopeAttributes = useCallback(() => {
setScopeAttributeFilters({ ...EMPTY_SCOPE_ATTRIBUTES })
}, [])
const filteredAlerts = useMemo(() => {
let result = MOCK_ALERTS
if (selectedType !== "all") {
result = result.filter((a) => a.type === selectedType)
}
if (scopeFilter === "multiple") {
result = result.filter((a) => a.scope === "multiple")
} else if (scopeFilter === "single") {
result = result.filter((a) => a.scope === "single")
}
if (activeFilter === "active") {
result = result.filter((a) => a.active)
} else if (activeFilter === "inactive") {
result = result.filter((a) => !a.active)
}
if (scopeFilter === "single") {
if (scopeAttributeFilters.customerId) {
result = result.filter((a) => a.customerId === scopeAttributeFilters.customerId)
}
if (scopeAttributeFilters.subscriptionId) {
result = result.filter((a) => a.subscriptionId === scopeAttributeFilters.subscriptionId)
}
if (scopeAttributeFilters.cadenceId) {
result = result.filter((a) => a.cadenceId === scopeAttributeFilters.cadenceId)
}
}
if (meterFilter) {
result = result.filter((a) => a.meter === meterFilter)
}
return result
}, [selectedType, scopeFilter, activeFilter, scopeAttributeFilters, meterFilter])
return (
<SidebarProvider>
<div className="flex min-h-screen w-full bg-white">
<AppSidebar />
<div className="flex flex-1 flex-col">
<DashboardHeader />
<div className="flex flex-1">
<main className="flex-1 overflow-auto bg-white">
<div className="flex flex-col gap-6 px-10 pt-6 pb-10">
<AlertsFilterBar
selectedType={selectedType}
onTypeChange={handleTypeChange}
scopeFilter={scopeFilter}
onScopeChange={handleScopeChange}
activeFilter={activeFilter}
onActiveChange={setActiveFilter}
scopeAttributeFilters={scopeAttributeFilters}
onScopeAttributeChange={handleScopeAttributeChange}
onClearScopeAttributes={handleClearScopeAttributes}
meterFilter={meterFilter}
onMeterChange={setMeterFilter}
allAlerts={MOCK_ALERTS}
totalResults={filteredAlerts.length}
/>
<AlertsTable
alerts={filteredAlerts}
onSelectAlert={setSelectedAlert}
selectedAlertId={selectedAlert?.id ?? null}
/>
</div>
</main>
{selectedAlert && (
<AlertDetailPanel
alert={selectedAlert}
onClose={() => setSelectedAlert(null)}
/>
)}
</div>
</div>
</div>
</SidebarProvider>
)
}components/alerts-filter-bar.tsx"use client"
import { useState, useMemo, useRef, useEffect } from "react"
import type { AlertType, Alert, ScopeFilterField } from "@/lib/alerts-data"
import {
SCOPE_FILTERS_BY_TYPE,
ALL_SCOPE_FILTERS,
SCOPE_FILTER_LABELS,
getUniqueValues,
getUniqueMeters,
} from "@/lib/alerts-data"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Plus, X, Check } from "lucide-react"
export type ScopeAttributeFilters = Record<ScopeFilterField, string | null>
interface AlertsFilterBarProps {
selectedType: AlertType | "all"
onTypeChange: (type: AlertType | "all") => void
scopeFilter: "all" | "multiple" | "single"
onScopeChange: (scope: "all" | "multiple" | "single") => void
activeFilter: "all" | "active" | "inactive"
onActiveChange: (filter: "all" | "active" | "inactive") => void
scopeAttributeFilters: ScopeAttributeFilters
onScopeAttributeChange: (field: ScopeFilterField, value: string | null) => void
onClearScopeAttributes: () => void
meterFilter: string | null
onMeterChange: (meter: string | null) => void
allAlerts: Alert[]
totalResults: number
}
function ScopeAttributeSelect({
field,
label: labelOverride,
selectedValue,
options,
onSelect,
}: {
field: ScopeFilterField
label?: string
selectedValue: string | null
options: { id: string; label: string }[]
onSelect: (value: string | null) => void
}) {
const displayLabel = labelOverride ?? SCOPE_FILTER_LABELS[field]
const [open, setOpen] = useState(false)
const [search, setSearch] = useState("")
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (open && inputRef.current) {
inputRef.current.focus()
}
}, [open])
const filtered = useMemo(() => {
if (!search.trim()) return options
const q = search.toLowerCase()
return options.filter((o) => o.label.toLowerCase().includes(q))
}, [search, options])
const selectedLabel = selectedValue
? options.find((o) => o.id === selectedValue)?.label ?? selectedValue
: null
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className={`inline-flex items-center gap-1.5 rounded-full border border-dashed px-3 h-6 text-xs font-medium transition-colors ${
selectedValue
? "border-primary/50 text-foreground bg-primary/5"
: "border-muted-foreground/40 text-muted-foreground hover:border-muted-foreground/60 hover:text-foreground"
}`}
>
<Plus className="h-3 w-3" />
<span>{selectedLabel ?? displayLabel}</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-64 p-0" align="start">
<div className="border-b p-2">
<Input
ref={inputRef}
placeholder={`Search ${displayLabel.toLowerCase()}...`}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-8 text-sm"
/>
</div>
<div className="max-h-52 overflow-auto p-1">
<button
type="button"
onClick={() => {
onSelect(null)
setOpen(false)
setSearch("")
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent"
>
<span className={`h-4 w-4 flex items-center justify-center ${!selectedValue ? "text-primary" : "text-transparent"}`}>
<Check className="h-3.5 w-3.5" />
</span>
<span className="text-muted-foreground italic">Any</span>
</button>
{filtered.length === 0 ? (
<div className="px-2 py-4 text-center text-xs text-muted-foreground">
No matches found
</div>
) : (
filtered.map((opt) => (
<button
type="button"
key={opt.id}
onClick={() => {
onSelect(opt.id === selectedValue ? null : opt.id)
setOpen(false)
setSearch("")
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent"
>
<span className={`h-4 w-4 flex items-center justify-center ${selectedValue === opt.id ? "text-primary" : "text-transparent"}`}>
<Check className="h-3.5 w-3.5" />
</span>
<span className="truncate font-mono text-xs">{opt.label}</span>
</button>
))
)}
</div>
</PopoverContent>
</Popover>
)
}
export function AlertsFilterBar({
selectedType,
onTypeChange,
scopeFilter,
onScopeChange,
activeFilter,
onActiveChange,
scopeAttributeFilters,
onScopeAttributeChange,
onClearScopeAttributes,
meterFilter,
onMeterChange,
allAlerts,
totalResults,
}: AlertsFilterBarProps) {
const hasActiveFilters =
selectedType !== "all" ||
scopeFilter !== "all" ||
activeFilter !== "all" ||
meterFilter !== null ||
Object.values(scopeAttributeFilters).some((v) => v !== null)
const showScopeAttributes = scopeFilter === "single"
const availableScopeFields: ScopeFilterField[] = useMemo(() => {
if (selectedType === "all") return ALL_SCOPE_FILTERS
return SCOPE_FILTERS_BY_TYPE[selectedType]
}, [selectedType])
const narrowAlerts = useMemo(() => {
let result = allAlerts.filter((a) => a.scope === "single")
if (selectedType !== "all") {
result = result.filter((a) => a.type === selectedType)
}
return result
}, [allAlerts, selectedType])
const showMeterFilter = selectedType === "usage" || selectedType === "all"
const meterOptions = useMemo(() => {
let base = allAlerts
if (scopeFilter !== "all") {
base = base.filter((a) => a.scope === scopeFilter)
}
return getUniqueMeters(base)
}, [allAlerts, scopeFilter])
const typeOptions = [
{ id: "all", label: "All" },
{ id: "spend", label: "Spend" },
{ id: "credit_balance", label: "Credit Balance" },
{ id: "usage", label: "Usage" },
]
const statusOptions = [
{ id: "all", label: "All" },
{ id: "active", label: "Active" },
{ id: "inactive", label: "Inactive" },
]
const scopeOptions = [
{ id: "all", label: "All" },
{ id: "multiple", label: "Multiple" },
{ id: "single", label: "Single" },
]
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-wrap">
<FilterPill
label="Type"
selectedValue={selectedType === "all" ? null : selectedType}
options={typeOptions}
onSelect={(val) => onTypeChange((val as AlertType) ?? "all")}
/>
<FilterPill
label="Status"
selectedValue={activeFilter === "all" ? null : activeFilter}
options={statusOptions}
onSelect={(val) => onActiveChange((val as "active" | "inactive") ?? "all")}
/>
<FilterPill
label="Scope"
selectedValue={scopeFilter === "all" ? null : scopeFilter}
options={scopeOptions}
onSelect={(val) => onScopeChange((val as "multiple" | "single") ?? "all")}
/>
{showMeterFilter && meterOptions.length > 0 && (
<ScopeAttributeSelect
field={"customerId" as ScopeFilterField}
label="Meter"
selectedValue={meterFilter}
options={meterOptions}
onSelect={onMeterChange}
/>
)}
{showScopeAttributes &&
availableScopeFields.map((field) => {
const options = getUniqueValues(narrowAlerts, field)
if (options.length === 0) return null
return (
<ScopeAttributeSelect
key={field}
field={field}
selectedValue={scopeAttributeFilters[field]}
options={options}
onSelect={(val) => onScopeAttributeChange(field, val)}
/>
)
})}
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => {
onTypeChange("all")
onScopeChange("all")
onActiveChange("all")
onClearScopeAttributes()
onMeterChange(null)
}}
>
Clear all
<X className="ml-1.5 h-3.5 w-3.5" />
</Button>
)}
</div>
<span className="text-sm text-muted-foreground shrink-0">
{totalResults} {totalResults === 1 ? "alert" : "alerts"}
</span>
</div>
</div>
)
}
function FilterPill({
label,
selectedValue,
options,
onSelect,
}: {
label: string
selectedValue: string | null
options: { id: string; label: string }[]
onSelect: (value: string | null) => void
}) {
const [open, setOpen] = useState(false)
const selectedLabel = selectedValue
? options.find((o) => o.id === selectedValue)?.label ?? selectedValue
: null
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className={`inline-flex items-center gap-1.5 rounded-full border border-dashed px-3 h-6 text-xs font-medium transition-colors ${
selectedValue
? "border-primary/50 text-foreground bg-primary/5"
: "border-muted-foreground/40 text-muted-foreground hover:border-muted-foreground/60 hover:text-foreground"
}`}
>
<Plus className="h-3 w-3" />
<span>{selectedLabel ?? label}</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-48 p-1" align="start">
{options.map((opt) => (
<button
type="button"
key={opt.id}
onClick={() => {
onSelect(opt.id === "all" ? null : opt.id)
setOpen(false)
}}
className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-accent"
>
<span className={`h-4 w-4 flex items-center justify-center ${(selectedValue === opt.id || (!selectedValue && opt.id === "all")) ? "text-primary" : "text-transparent"}`}>
<Check className="h-3.5 w-3.5" />
</span>
<span>{opt.label}</span>
</button>
))}
</PopoverContent>
</Popover>
)
}components/alerts-table.tsx"use client"
import type { Alert } from "@/lib/alerts-data"
import { ALERT_TYPE_LABELS } from "@/lib/alerts-data"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { MoreHorizontal, Square, Layers } from "lucide-react"
interface AlertsTableProps {
alerts: Alert[]
onSelectAlert: (alert: Alert) => void
selectedAlertId: string | null
}
export function AlertsTable({
alerts,
onSelectAlert,
selectedAlertId,
}: AlertsTableProps) {
return (
<div className="bg-white">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent h-10">
<TableHead className="w-[280px] text-foreground font-medium py-2">Name</TableHead>
<TableHead className="w-[100px] text-foreground font-medium py-2">Status</TableHead>
<TableHead className="w-[140px] text-foreground font-medium py-2">Type</TableHead>
<TableHead className="w-[180px] text-foreground font-medium py-2">Trigger amount</TableHead>
<TableHead className="w-[200px] text-foreground font-medium py-2">Applies to</TableHead>
<TableHead className="w-[50px] py-2">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{alerts.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
No alerts match your current filters.
</TableCell>
</TableRow>
) : (
alerts.map((alert) => (
<TableRow
key={alert.id}
className={`cursor-pointer transition-colors bg-white h-10 ${
selectedAlertId === alert.id ? "bg-accent" : "hover:bg-muted/30"
}`}
onClick={() => onSelectAlert(alert)}
>
<TableCell className="py-2">
<span className="font-semibold text-foreground">{alert.name}</span>
</TableCell>
<TableCell className="py-2">
<Badge
variant="outline"
className={`font-medium text-xs ${
alert.active
? "bg-emerald-50 text-emerald-600 border-emerald-200"
: "bg-muted text-muted-foreground border-border"
}`}
>
{alert.active ? "Active" : "Inactive"}
</Badge>
</TableCell>
<TableCell className="py-2">
<span className="text-sm text-foreground">{ALERT_TYPE_LABELS[alert.type]}</span>
</TableCell>
<TableCell className="py-2">
<span className="text-sm text-foreground">{alert.threshold}</span>
</TableCell>
<TableCell className="py-2">
<div className="flex items-center gap-2">
{alert.scope === "single" ? (
<Square className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
) : (
<Layers className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
)}
<span className="text-sm text-foreground">
{alert.scope === "single" && alert.customerName
? alert.customerName
: alert.conditions === "All customers"
? "Any customer"
: alert.conditions}
</span>
</div>
</TableCell>
<TableCell className="py-2">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)
}components/alert-detail-panel.tsx"use client"
import React from "react"
import type { Alert, AlertType } from "@/lib/alerts-data"
import {
ALERT_TYPE_LABELS,
GROUP_BY_LABELS,
AGGREGATION_PERIOD_LABELS,
} from "@/lib/alerts-data"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import {
X,
Copy,
ExternalLink,
DollarSign,
CreditCard,
Activity,
Layers,
Target,
} from "lucide-react"
interface AlertDetailPanelProps {
alert: Alert
onClose: () => void
}
const TYPE_ICONS: Record<AlertType, typeof DollarSign> = {
spend: DollarSign,
credit_balance: CreditCard,
usage: Activity,
}
const TYPE_COLORS: Record<AlertType, string> = {
spend: "bg-[hsl(var(--alert-spend-bg))] text-[hsl(var(--alert-spend))] border-[hsl(var(--alert-spend))]/20",
credit_balance: "bg-[hsl(var(--alert-credit-bg))] text-[hsl(var(--alert-credit))] border-[hsl(var(--alert-credit))]/20",
usage: "bg-[hsl(var(--alert-usage-bg))] text-[hsl(var(--alert-usage))] border-[hsl(var(--alert-usage))]/20",
}
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-start justify-between py-2.5">
<span className="text-sm text-muted-foreground shrink-0">{label}</span>
<div className="text-sm text-foreground text-right">{children}</div>
</div>
)
}
export function AlertDetailPanel({ alert, onClose }: AlertDetailPanelProps) {
const Icon = TYPE_ICONS[alert.type]
return (
<div className="w-[380px] shrink-0 border-l bg-card overflow-y-auto">
<div className="flex items-center justify-between p-5 border-b">
<h3 className="text-base font-semibold text-foreground">Alert Details</h3>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={onClose}>
<X className="h-4 w-4" />
<span className="sr-only">Close panel</span>
</Button>
</div>
<div className="p-5 flex flex-col gap-5">
<div className="flex flex-col gap-3">
<div className="flex items-start gap-3">
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${
alert.type === "spend" ? "bg-[hsl(var(--alert-spend-bg))]"
: alert.type === "credit_balance" ? "bg-[hsl(var(--alert-credit-bg))]"
: "bg-[hsl(var(--alert-usage-bg))]"
}`}>
<Icon className={`h-5 w-5 ${
alert.type === "spend" ? "text-[hsl(var(--alert-spend))]"
: alert.type === "credit_balance" ? "text-[hsl(var(--alert-credit))]"
: "text-[hsl(var(--alert-usage))]"
}`} />
</div>
<div className="flex flex-col gap-1 min-w-0">
<h4 className="text-base font-semibold text-foreground leading-tight">{alert.name}</h4>
<button type="button" className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors group">
<span className="font-mono">{alert.id}</span>
<Copy className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
</button>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className={TYPE_COLORS[alert.type]}>{ALERT_TYPE_LABELS[alert.type]}</Badge>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
{alert.scope === "multiple" ? (
<>
<Layers className="h-3 w-3" />
<span>Multiple</span>
</>
) : (
<>
<Target className="h-3 w-3" />
<span>Single</span>
</>
)}
</div>
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-foreground">Status</span>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">{alert.active ? "Active" : "Inactive"}</span>
<Switch checked={alert.active} aria-label="Toggle alert status" />
</div>
</div>
<Separator />
<div className="flex flex-col gap-1">
<h5 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1">Configuration</h5>
<DetailRow label="Threshold"><span className="font-mono font-medium">{alert.threshold}</span></DetailRow>
<DetailRow label="Group By"><span className="text-sm">{GROUP_BY_LABELS[alert.groupBy]}</span></DetailRow>
{alert.aggregationPeriod && (
<DetailRow label="Aggregation"><span className="text-sm">{AGGREGATION_PERIOD_LABELS[alert.aggregationPeriod]}</span></DetailRow>
)}
<DetailRow label="Conditions">
<span className={alert.conditions === "All customers" ? "italic text-muted-foreground" : ""}>{alert.conditions}</span>
</DetailRow>
{alert.meter && (
<DetailRow label="Meter">
<span className="font-mono text-xs bg-secondary px-1.5 py-0.5 rounded">{alert.meter}</span>
</DetailRow>
)}
</div>
<Separator />
<div className="flex flex-col gap-1">
<h5 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1">Activity</h5>
<DetailRow label="Last fired">
{alert.lastFired ? <span>{alert.lastFired}</span> : <span className="italic text-muted-foreground">Never</span>}
</DetailRow>
<DetailRow label="Created">{alert.createdAt}</DetailRow>
</div>
<Separator />
<div className="flex flex-col gap-2">
<Button variant="outline" className="w-full justify-start gap-2 bg-transparent">
<ExternalLink className="h-3.5 w-3.5" />
View in API
</Button>
<Button variant="outline" className="w-full justify-start gap-2 text-destructive hover:text-destructive hover:bg-destructive/5 border-destructive/20 bg-transparent">
<X className="h-3.5 w-3.5" />
Delete alert
</Button>
</div>
</div>
</div>
)
}components/dashboard-header.tsx"use client"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Plus, Search, Settings, HelpCircle, LayoutGrid } from "lucide-react"
const NAV_TABS = [
{ label: "Pricing plans", href: "#" },
{ label: "Pricing plan subscriptions", href: "#" },
{ label: "Meters", href: "#" },
{ label: "Alerts", href: "#", active: true },
{ label: "Credits", href: "#" },
]
export function DashboardHeader() {
return (
<header className="bg-card">
<div className="flex items-center justify-between px-10 py-3 border-b">
<div className="flex items-center gap-3">
<div className="relative max-w-xs w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="text"
placeholder="Search"
className="w-full h-8 pl-9 pr-3 text-sm rounded-md border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<LayoutGrid className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<HelpCircle className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex items-center justify-between px-10 pt-5 pb-4">
<h1 className="text-2xl font-semibold tracking-tight text-foreground">Usage-based billing</h1>
<Button className="gap-2">
<Plus className="h-4 w-4" />
Create alert
</Button>
</div>
<nav className="px-10" aria-label="Billing sections">
<div className="flex items-center gap-1 border-b">
{NAV_TABS.map((tab) => (
<a
key={tab.label}
href={tab.href}
className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors first:pl-0 ${
tab.active
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
}`}
>
{tab.label}
</a>
))}
</div>
</nav>
</header>
)
}components/app-sidebar.tsx"use client"
import { useState } from "react"
import {
Home, SlidersHorizontal, ArrowLeftRight, Users, Package, Clock,
Layers, CreditCard, FileText, BarChart3, MoreHorizontal, ChevronDown,
} from "lucide-react"
import {
Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent,
SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton,
SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem,
} from "@/components/ui/sidebar"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
const mainNavItems = [
{ label: "Home", icon: Home },
{ label: "Balances", icon: SlidersHorizontal },
{ label: "Transactions", icon: ArrowLeftRight },
{ label: "Customers", icon: Users },
{ label: "Product catalog", icon: Package },
]
const shortcutItems = [
{ label: "Disputes", icon: Clock },
{ label: "Tax", icon: Clock },
{ label: "Reports", icon: Clock },
]
const productItems = [
{ label: "Connect", icon: Layers, items: [] },
{ label: "Payments", icon: CreditCard, items: [] },
{
label: "Billing",
icon: FileText,
items: [
{ label: "Overview" },
{ label: "Subscriptions" },
{ label: "Invoices" },
{ label: "Usage-based", active: true },
{ label: "Revenue recovery" },
],
},
{ label: "Reporting", icon: BarChart3, items: [] },
{ label: "More", icon: MoreHorizontal, items: [] },
]
export function AppSidebar() {
const [openProducts, setOpenProducts] = useState<string[]>(["Billing"])
const toggleProduct = (label: string) => {
setOpenProducts((prev) =>
prev.includes(label) ? prev.filter((item) => item !== label) : [...prev, label]
)
}
return (
<Sidebar collapsible="none" className="border-r border-border bg-white">
<SidebarHeader className="p-4 pb-2">
<button type="button" className="flex items-center gap-3 rounded-md px-2 py-1.5 hover:bg-accent transition-colors">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-100 text-lg">🌵</div>
<span className="text-sm font-semibold text-foreground">Cactus Practice</span>
<ChevronDown className="ml-auto h-4 w-4 text-muted-foreground" />
</button>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{mainNavItems.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Shortcuts</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{shortcutItems.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Products</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{productItems.map((product) => (
<Collapsible
key={product.label}
open={openProducts.includes(product.label)}
onOpenChange={() => toggleProduct(product.label)}
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<product.icon className="h-4 w-4" />
<span>{product.label}</span>
<ChevronDown className={`ml-auto h-4 w-4 text-muted-foreground transition-transform ${openProducts.includes(product.label) ? "rotate-180" : ""}`} />
</SidebarMenuButton>
</CollapsibleTrigger>
{product.items.length > 0 && (
<CollapsibleContent>
<SidebarMenuSub>
{product.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.label}>
<SidebarMenuSubButton className={subItem.active ? "text-primary font-medium" : ""}>
<span>{subItem.label}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
)}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
)
}You'll also need to add these CSS variables to your globals.css for the alert type colors:
:root {
--alert-spend: 142 76% 36%;
--alert-spend-bg: 142 76% 96%;
--alert-credit: 217 91% 60%;
--alert-credit-bg: 217 91% 96%;
--alert-usage: 280 68% 60%;
--alert-usage-bg: 280 68% 96%;
}These 8 files contain everything needed for the alerts tab. The components use shadcn/ui components (Button, Badge, Table, Popover, Sidebar, etc.) so make sure those are installed in your target project.
You can send messages while the preview loads