ProjectOverview
surface프로젝트 개요 Surface. SummaryRow + NavCards + LineChart + BarChart + DataTable + CodeSnippetViewer + Dashboard CRUD 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Project Overview
Total Events1.2M↑ +12%
Active Users8,420↑ +5.3%
Sessions24.5K↓ -2.1%
Error Rate0.12%↓ -8%
Trends
Breakdown
Top Events
Data Table
| Event | Count | Change |
|---|---|---|
| page_view | 45,200 | +8.2% |
| button_click | 32,100 | -3.1% |
| form_submit | 18,400 | +12.5% |
| api_request | 12,300 | +1.8% |
| error_thrown | 1,520 | -15.2% |
Recent Events
Data Table
| Event | Time | User |
|---|---|---|
| page_view | 2026-02-23T10:30:00Z | user_abc |
| button_click | 2026-02-23T10:29:45Z | user_def |
| form_submit | 2026-02-23T10:29:30Z | user_abc |
| api_request | 2026-02-23T10:29:15Z | user_ghi |
| error_thrown | 2026-02-23T10:29:00Z | user_jkl |
Dashboards
Data Table
| Name | Last Updated |
|---|---|
| Marketing Overview | 2026-02-22T08:00:00Z |
| Engineering Metrics | 2026-02-21T16:30:00Z |
| Growth Dashboard | 2026-02-20T12:00:00Z |
SDK Integration
Installation
1import posthog from 'posthog-js'2posthog.init('phc_xxx', { api_host: 'https://app.posthog.com' })테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
ProjectOverview 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
ProjectOverview Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
stats | StatCardType[] | — | 통계 카드 배열 (2-4) |
navCards | ProjectOverviewNavCard[] | — | 분석 도구 내비게이션 카드 |
trendData | ChartDataPoint[] | — | 시계열 차트 데이터 |
trendSeries | ChartSeriesDef[] | — | 시계열 시리즈 정의 |
barData | ChartDataPoint[] | — | 바 차트 데이터 |
barSeries | ChartSeriesDef[] | — | 바 차트 시리즈 정의 |
topEvents | ProjectOverviewTopEvent[] | — | 상위 이벤트 테이블 |
recentEvents | ProjectOverviewRecentEvent[] | — | 최근 이벤트 리스트 |
dashboards | ProjectOverviewDashboard[] | — | 대시보드 목록 |
onDashboardCreate | () => void | — | 대시보드 생성 핸들러 |
onDashboardClick | (id: string) => void | — | 대시보드 클릭 핸들러 |
onDashboardDelete | (id: string) => void | — | 대시보드 삭제 핸들러 |
sdkSnippet | string | — | SDK 코드 스니펫 |
sdkLanguage | string | "typescript" | 스니펫 언어 |
timeRange | DateRange | — | 시간 범위 필터 |
onTimeRangeChange | (range: DateRange) => void | — | 시간 범위 변경 핸들러 |
loading | boolean | — | 로딩 상태 |
header | ReactNode | — | 커스텀 헤더 슬롯 |
actions | ReactNode | — | 액션 버튼 슬롯 |
labels | ProjectOverviewLabels | — | 커스텀 레이블 |
className | string | — | 최외곽 CSS 클래스 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add project-overviewConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { ProjectOverview } from "@/components/surfaces/project-overview";Registry metadata
- 설명
- 프로젝트 개요 Surface. SummaryRow + NavCards + LineChart + BarChart + DataTable + CodeSnippetViewer + Dashboard CRUD 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- charttabledashboard
- Install notes
- 없음
포함 파일
project-overview.tsxproject-overview.tsx
Surface 소스 보기
project-overview.tsx
"use client";
import * as React from "react";
import {
Button,
DataTable,
CodeSnippetViewer,
SurfaceLayout,
SummaryRow,
TimeRangeSelector,
ConfirmDialog,
cn,
type ColumnDef,
type DateRange,
type StatCardType,
} from "@reopt-ai/opt-ui";
import {
LineChart,
BarChart,
type ChartDataPoint,
type ChartSeriesDef,
} from "@reopt-ai/opt-charts";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
/** Card data for `ProjectOverviewNav`. */
export interface ProjectOverviewNavCard {
id: string;
label: string;
description?: string;
icon?: React.ReactNode;
href?: string;
onClick?: () => void;
}
/** Event data for `ProjectOverviewTop`. */
export interface ProjectOverviewTopEvent {
name: string;
count: number;
change?: number;
}
/** Event data for `ProjectOverviewRecent`. */
export interface ProjectOverviewRecentEvent {
name: string;
timestamp: string;
userId?: string;
}
/** Dashboard data for `ProjectOverviewDashboard`. */
export interface ProjectOverviewDashboard {
id: string;
name: string;
updatedAt?: string;
}
/** Labels for `ProjectOverview`. */
export interface ProjectOverviewLabels {
overview?: string;
analytics?: string;
trends?: string;
barChart?: string;
topEvents?: string;
recentEvents?: string;
dashboards?: string;
sdk?: string;
installation?: string;
createDashboard?: string;
deleteDashboard?: string;
deleteConfirmTitle?: string;
deleteConfirmDescription?: string;
deleteConfirmButton?: string;
cancelButton?: string;
emptyTitle?: string;
emptyDescription?: string;
noDashboards?: string;
eventName?: string;
eventCount?: string;
eventChange?: string;
recentEventName?: string;
recentTimestamp?: string;
recentUserId?: string;
dashboardName?: string;
dashboardUpdated?: string;
dashboardActions?: string;
}
const defaultLabels: Required<ProjectOverviewLabels> = {
overview: "Project Overview",
analytics: "Analysis Tools",
trends: "Trends",
barChart: "Breakdown",
topEvents: "Top Events",
recentEvents: "Recent Events",
dashboards: "Dashboards",
sdk: "SDK Integration",
installation: "Installation",
createDashboard: "Create Dashboard",
deleteDashboard: "Delete",
deleteConfirmTitle: "Delete Dashboard",
deleteConfirmDescription:
"Are you sure you want to delete this dashboard? This action cannot be undone.",
deleteConfirmButton: "Delete",
cancelButton: "Cancel",
emptyTitle: "No data yet",
emptyDescription: "Project overview data will appear here once available.",
noDashboards: "No dashboards created yet.",
eventName: "Event",
eventCount: "Count",
eventChange: "Change",
recentEventName: "Event",
recentTimestamp: "Time",
recentUserId: "User",
dashboardName: "Name",
dashboardUpdated: "Last Updated",
dashboardActions: "Actions",
};
/* ------------------------------------------------------------------ */
/* Props */
/* ------------------------------------------------------------------ */
/** Props for `ProjectOverview`. */
export interface ProjectOverviewProps {
/** Overview stat cards (2-4) */
stats?: StatCardType[];
/** Analysis tool navigation cards */
navCards?: ProjectOverviewNavCard[];
/** Time series chart data */
trendData?: ChartDataPoint[];
/** Series definitions for trend chart */
trendSeries?: ChartSeriesDef[];
/** Bar chart data */
barData?: ChartDataPoint[];
/** Bar chart series definitions */
barSeries?: ChartSeriesDef[];
/** Top events table data */
topEvents?: ProjectOverviewTopEvent[];
/** Recent events list */
recentEvents?: ProjectOverviewRecentEvent[];
/** Saved dashboards */
dashboards?: ProjectOverviewDashboard[];
/** Create dashboard callback */
onDashboardCreate?: () => void;
/** Dashboard click callback */
onDashboardClick?: (id: string) => void;
/** Dashboard delete callback */
onDashboardDelete?: (id: string) => void;
/** SDK integration code snippet */
sdkSnippet?: string;
/** Snippet language */
sdkLanguage?: string;
/** Date range value */
timeRange?: DateRange;
/** Date range change handler */
onTimeRangeChange?: (range: DateRange) => void;
/** Loading state */
loading?: boolean;
/** Custom header slot */
header?: React.ReactNode;
/** Custom actions slot */
actions?: React.ReactNode;
/** Custom labels */
labels?: ProjectOverviewLabels;
/** Root className override */
className?: string;
}
/* ------------------------------------------------------------------ */
/* Default columns */
/* ------------------------------------------------------------------ */
function buildTopEventColumns(
labels: Required<ProjectOverviewLabels>,
): ColumnDef<ProjectOverviewTopEvent>[] {
return [
{ id: "name", header: labels.eventName, accessor: "name" },
{
id: "count",
header: labels.eventCount,
accessor: (row) => row.count.toLocaleString(),
},
{
id: "change",
header: labels.eventChange,
accessor: (row) =>
row.change != null
? `${row.change > 0 ? "+" : ""}${row.change}%`
: "\u2014",
},
];
}
function buildRecentEventColumns(
labels: Required<ProjectOverviewLabels>,
): ColumnDef<ProjectOverviewRecentEvent>[] {
const cols: ColumnDef<ProjectOverviewRecentEvent>[] = [
{ id: "name", header: labels.recentEventName, accessor: "name" },
{ id: "timestamp", header: labels.recentTimestamp, accessor: "timestamp" },
];
cols.push({
id: "userId",
header: labels.recentUserId,
accessor: (row) => row.userId ?? "\u2014",
});
return cols;
}
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
/** Renders the `ProjectOverview` component. */
export function ProjectOverview({
stats = [],
navCards = [],
trendData = [],
trendSeries = [],
barData = [],
barSeries = [],
topEvents = [],
recentEvents = [],
dashboards = [],
onDashboardCreate,
onDashboardClick,
onDashboardDelete,
sdkSnippet,
sdkLanguage = "typescript",
timeRange,
onTimeRangeChange,
loading = false,
header,
actions,
labels: customLabels,
className,
}: ProjectOverviewProps) {
const labels = { ...defaultLabels, ...customLabels };
const titleId = React.useId();
const analyticsId = React.useId();
const trendsId = React.useId();
const barId = React.useId();
const topEventsId = React.useId();
const recentEventsId = React.useId();
const dashboardsId = React.useId();
const sdkId = React.useId();
const [pendingDeleteId, setPendingDeleteId] = React.useState<string | null>(
null,
);
const topEventColumns = React.useMemo(
() => buildTopEventColumns(labels),
[labels.eventName, labels.eventCount, labels.eventChange],
);
const recentEventColumns = React.useMemo(
() => buildRecentEventColumns(labels),
[labels.recentEventName, labels.recentTimestamp, labels.recentUserId],
);
const dashboardColumns = React.useMemo<ColumnDef<ProjectOverviewDashboard>[]>(
() => [
{ id: "name", header: labels.dashboardName, accessor: "name" },
{
id: "updatedAt",
header: labels.dashboardUpdated,
accessor: (row) => row.updatedAt ?? "\u2014",
},
...(onDashboardDelete
? [
{
id: "actions" as const,
header: labels.dashboardActions,
accessor: () => "",
cell: (row: ProjectOverviewDashboard) => (
<Button
variant="ghost"
size="sm"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
setPendingDeleteId(row.id);
}}
>
{labels.deleteDashboard}
</Button>
),
},
]
: []),
],
[
labels.dashboardName,
labels.dashboardUpdated,
labels.dashboardActions,
labels.deleteDashboard,
onDashboardDelete,
],
);
const hasStats = stats.length > 0;
const hasNavCards = navCards.length > 0;
const hasTrend = trendData.length > 0 && trendSeries.length > 0;
const hasBar = barData.length > 0 && barSeries.length > 0;
const hasTopEvents = topEvents.length > 0;
const hasRecentEvents = recentEvents.length > 0;
const hasDashboards = dashboards.length > 0;
const isEmpty = !hasStats && !hasNavCards && !hasTrend && !hasBar;
const handleConfirmDelete = () => {
if (pendingDeleteId != null) {
onDashboardDelete?.(pendingDeleteId);
setPendingDeleteId(null);
}
};
return (
<SurfaceLayout loading={loading} className={className}>
{/* ---- Header ---- */}
{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.overview}
</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>
) : (
<>
{/* ---- Stats Cards ---- */}
{hasStats && <SummaryRow stats={stats} columns={4} />}
{/* ---- Navigation Cards ---- */}
{hasNavCards && (
<section aria-labelledby={analyticsId}>
<h3
id={analyticsId}
className="text-text-tertiary mb-3 text-sm font-medium"
>
{labels.analytics}
</h3>
<nav
aria-label={labels.analytics}
className="gap-group grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"
>
{navCards.map((card) => {
const content = (
<>
{card.icon && (
<span className="text-lg" aria-hidden="true">
{card.icon}
</span>
)}
<span className="text-text-primary text-sm font-medium">
{card.label}
</span>
{card.description && (
<span className="text-text-tertiary text-xs">
{card.description}
</span>
)}
</>
);
const cls =
"gap-element flex flex-col rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-subtle";
if (card.href) {
return (
<a key={card.id} href={card.href} className={cls}>
{content}
</a>
);
}
return (
<button
key={card.id}
type="button"
onClick={card.onClick}
className={cn(cls, "text-left")}
>
{content}
</button>
);
})}
</nav>
</section>
)}
{/* ---- Charts Section ---- */}
{(hasTrend || hasBar) && (
<div className="gap-section grid grid-cols-1 lg:grid-cols-2">
{hasTrend && (
<section aria-labelledby={trendsId}>
<h3
id={trendsId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.trends}
</h3>
<LineChart
data={trendData}
series={trendSeries}
aria-label={labels.trends}
/>
</section>
)}
{hasBar && (
<section aria-labelledby={barId}>
<h3
id={barId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.barChart}
</h3>
<BarChart
data={barData}
series={barSeries}
aria-label={labels.barChart}
/>
</section>
)}
</div>
)}
{/* ---- Events Section ---- */}
{(hasTopEvents || hasRecentEvents) && (
<div className="gap-section grid grid-cols-1 lg:grid-cols-2">
{hasTopEvents && (
<section aria-labelledby={topEventsId}>
<h3
id={topEventsId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.topEvents}
</h3>
<DataTable
columns={topEventColumns}
data={topEvents}
keyExtractor={(r) => r.name}
/>
</section>
)}
{hasRecentEvents && (
<section aria-labelledby={recentEventsId}>
<h3
id={recentEventsId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.recentEvents}
</h3>
<DataTable
columns={recentEventColumns}
data={recentEvents}
keyExtractor={(r) => `${r.name}-${r.timestamp}`}
/>
</section>
)}
</div>
)}
{/* ---- Dashboards Section ---- */}
{(hasDashboards || onDashboardCreate) && (
<section aria-labelledby={dashboardsId}>
<div className="mb-2 flex items-center justify-between">
<h3
id={dashboardsId}
className="text-text-tertiary text-sm font-medium"
>
{labels.dashboards}
</h3>
{onDashboardCreate && (
<Button
variant="primary"
size="sm"
onClick={onDashboardCreate}
>
+ {labels.createDashboard}
</Button>
)}
</div>
{hasDashboards ? (
<DataTable
columns={dashboardColumns}
data={dashboards}
keyExtractor={(r) => r.id}
onRowClick={
onDashboardClick
? (row) => onDashboardClick(row.id)
: undefined
}
/>
) : (
<p className="text-text-tertiary py-4 text-center text-sm">
{labels.noDashboards}
</p>
)}
</section>
)}
{/* ---- SDK Snippet Section ---- */}
{sdkSnippet && (
<section aria-labelledby={sdkId}>
<h3
id={sdkId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.sdk}
</h3>
<CodeSnippetViewer
code={sdkSnippet}
language={sdkLanguage}
title={labels.installation}
/>
</section>
)}
</>
)}
{/* ---- Delete Dashboard Confirmation ---- */}
<ConfirmDialog
open={pendingDeleteId != null}
onConfirm={handleConfirmDelete}
onCancel={() => setPendingDeleteId(null)}
title={labels.deleteConfirmTitle}
description={labels.deleteConfirmDescription}
confirmLabel={labels.deleteConfirmButton}
cancelLabel={labels.cancelButton}
variant="danger"
/>
</SurfaceLayout>
);
}
ProjectOverview.displayName = "ProjectOverview";