AnalyticsDashboard
surface분석 대시보드 Surface. StatCard + LineChart + BarChart 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Analytics
Total Users12,345↑ +12.5%
Events1.2M↑ +8.3%
Sessions45,678↓ -2.1%
Retention68%↑ +1.2%
Trend
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
AnalyticsDashboard 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
AnalyticsDashboard Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
stats* | AnalyticsDashboardStat[] | — | 통계 카드 배열 |
trendData | ChartDataPoint[] | — | 추세 차트 데이터 |
trendSeries | ChartSeriesDef[] | — | 추세 시리즈 정의 |
breakdownData | ChartDataPoint[] | — | 분석 차트 데이터 |
breakdownSeries | ChartSeriesDef[] | — | 분석 시리즈 정의 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add analytics-dashboardConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { AnalyticsDashboard } from "@/components/surfaces/analytics-dashboard";Registry metadata
- 설명
- 분석 대시보드 Surface. StatCard + LineChart + BarChart 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- chartdashboardanalytics
- Install notes
- 없음
포함 파일
analytics-dashboard.tsxanalytics-dashboard.tsx
Surface 소스 보기
analytics-dashboard.tsx
"use client";
import * as React from "react";
import {
SurfaceLayout,
SummaryRow,
TimeRangeSelector,
cn,
type DateRange,
} from "@reopt-ai/opt-ui";
import {
LineChart,
BarChart,
PieChart,
type ChartDataPoint,
type ChartSeriesDef,
type PieChartDataPoint,
} from "@reopt-ai/opt-charts";
/** Statistic shape for `AnalyticsDashboard`. */
export interface AnalyticsDashboardStat {
id: string;
title: string;
value: string;
change: string;
trend: "up" | "down" | "neutral";
sparklineData?: number[];
}
/** Item shape for `AnalyticsNav`. */
export interface AnalyticsNavItem {
id: string;
label: string;
description?: string;
icon?: string;
href?: string;
onClick?: () => void;
}
/** Labels for `AnalyticsDashboard`. */
export interface AnalyticsDashboardLabels {
analytics?: string;
trend?: string;
breakdown?: string;
pie?: string;
tools?: string;
emptyTitle?: string;
emptyDescription?: string;
}
const defaultLabels: Required<AnalyticsDashboardLabels> = {
analytics: "Analytics",
trend: "Trend",
breakdown: "Breakdown",
pie: "Distribution",
tools: "Analysis Tools",
emptyTitle: "No analytics data",
emptyDescription: "Analytics data will appear here once available.",
};
/** Props for `AnalyticsDashboard`. */
export interface AnalyticsDashboardProps {
stats: AnalyticsDashboardStat[];
trendData?: ChartDataPoint[];
trendSeries?: ChartSeriesDef[];
breakdownData?: ChartDataPoint[];
breakdownSeries?: ChartSeriesDef[];
pieData?: PieChartDataPoint[];
/** Navigation cards for analysis tools */
navItems?: AnalyticsNavItem[];
/** Footer content (e.g., SDK info, integration details) */
footer?: React.ReactNode;
header?: React.ReactNode;
actions?: React.ReactNode;
/** Custom filter/alert area rendered between stats and charts (e.g., metric selector badges, callouts) */
filterContent?: React.ReactNode;
/** Additional content rendered between charts and nav items (e.g., top events table, recent activity) */
tableContent?: React.ReactNode;
labels?: AnalyticsDashboardLabels;
loading?: boolean;
timeRange?: DateRange;
onTimeRangeChange?: (range: DateRange) => void;
className?: string;
}
/** Renders the `AnalyticsDashboard` component. */
export function AnalyticsDashboard({
stats,
trendData = [],
trendSeries = [],
breakdownData = [],
breakdownSeries = [],
pieData,
navItems,
footer,
header,
actions,
filterContent,
tableContent,
labels: customLabels,
loading = false,
timeRange,
onTimeRangeChange,
className,
}: AnalyticsDashboardProps) {
const labels = { ...defaultLabels, ...customLabels };
const titleId = React.useId();
const trendId = React.useId();
const breakdownId = React.useId();
const pieId = React.useId();
const toolsId = React.useId();
const isEmpty = stats.length === 0;
const hasNavItems = navItems && navItems.length > 0;
return (
<SurfaceLayout loading={loading} className={className}>
{header || actions ? (
<div className="flex items-center justify-between">
{header && <div>{header}</div>}
<div className="gap-element flex items-center">
<TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
{actions}
</div>
</div>
) : (
<div className="flex items-center justify-between">
<h2 id={titleId} className="text-text-primary text-lg font-semibold">
{labels.analytics}
</h2>
<TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
</div>
)}
{isEmpty ? (
<div
role="status"
className="flex flex-col items-center justify-center py-16 text-center"
>
<h3 className="text-text-primary text-lg font-medium">
{labels.emptyTitle}
</h3>
<p className="text-text-tertiary mt-1 text-sm">
{labels.emptyDescription}
</p>
</div>
) : (
<>
<SummaryRow stats={stats} columns={4} />
{filterContent}
{trendData.length > 0 && trendSeries.length > 0 && (
<section aria-labelledby={trendId}>
<h3
id={trendId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.trend}
</h3>
<LineChart
data={trendData}
series={trendSeries}
aria-label={labels.trend}
/>
</section>
)}
{breakdownData.length > 0 && breakdownSeries.length > 0 && (
<section aria-labelledby={breakdownId}>
<h3
id={breakdownId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.breakdown}
</h3>
<BarChart
data={breakdownData}
series={breakdownSeries}
aria-label={labels.breakdown}
/>
</section>
)}
{pieData && pieData.length > 0 && (
<section aria-labelledby={pieId}>
<h3
id={pieId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.pie}
</h3>
<PieChart data={pieData} aria-label={labels.pie} />
</section>
)}
{tableContent}
{hasNavItems && (
<section aria-labelledby={toolsId}>
<h3
id={toolsId}
className="text-text-tertiary mb-3 text-sm font-medium"
>
{labels.tools}
</h3>
<nav
aria-label={labels.tools}
className="gap-group grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"
>
{navItems.map((item) => {
const content = (
<>
{item.icon && (
<span className="text-lg" aria-hidden="true">
{item.icon}
</span>
)}
<span className="text-text-primary text-sm font-medium">
{item.label}
</span>
{item.description && (
<span className="text-text-tertiary text-xs">
{item.description}
</span>
)}
</>
);
const cls =
"gap-element flex flex-col rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-subtle";
if (item.href) {
return (
<a key={item.id} href={item.href} className={cls}>
{content}
</a>
);
}
return (
<button
key={item.id}
type="button"
onClick={item.onClick}
className={cn(cls, "text-left")}
>
{content}
</button>
);
})}
</nav>
</section>
)}
{footer && <div>{footer}</div>}
</>
)}
</SurfaceLayout>
);
}
AnalyticsDashboard.displayName = "AnalyticsDashboard";