RealtimeDashboard
surface실시간 대시보드 Surface. ConnectionIndicator + StatCard + ActivityFeed 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Real-time
Connected
Active Users342↑ +28
Events/sec1,205↑ +15%
Errors3↓ -2
Latency45ms↓ -5ms
Live Events
활동 피드
U
user_123 생성 page_view by user_123
U
user_456 생성 signup — Pro Plan
U
user_789 완료 purchase $49.99
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
RealtimeDashboard 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
RealtimeDashboard Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
status* | ConnectionStatus | — | 연결 상태 |
stats* | RealtimeDashboardStat[] | — | 통계 배열 |
activities* | ActivityDef[] | — | 활동 피드 배열 |
onLoadMore | () => void | — | 더 불러오기 핸들러 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add realtime-dashboardConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { RealtimeDashboard } from "@/components/surfaces/realtime-dashboard";Registry metadata
- 설명
- 실시간 대시보드 Surface. ConnectionIndicator + StatCard + ActivityFeed 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- dashboard
- Install notes
- 없음
포함 파일
realtime-dashboard.tsxrealtime-dashboard.tsx
Surface 소스 보기
realtime-dashboard.tsx
"use client";
import * as React from "react";
import {
ConnectionIndicator,
ActivityFeed,
SurfaceLayout,
SummaryRow,
TimeRangeSelector,
cn,
type ConnectionStatus,
type ActivityDef,
type DateRange,
} from "@reopt-ai/opt-ui";
/** Statistic shape for `RealtimeDashboard`. */
export interface RealtimeDashboardStat {
id: string;
title: string;
value: string;
change: string;
trend: "up" | "down" | "neutral";
/** Optional variant for gradient-styled stat cards */
variant?: "default" | "gradient";
}
/** Item shape for `TopEvent`. */
export interface TopEventItem {
id: string;
name: string;
count: number;
}
/** Labels for `RealtimeDashboard`. */
export interface RealtimeDashboardLabels {
realtime?: string;
liveEvents?: string;
topEvents?: string;
loadMore?: string;
emptyTitle?: string;
emptyDescription?: string;
}
const defaultLabels: Required<RealtimeDashboardLabels> = {
realtime: "Real-time",
liveEvents: "Live Events",
topEvents: "Top Events",
loadMore: "Load more",
emptyTitle: "No real-time data",
emptyDescription: "Live events and stats will appear here once available.",
};
/** Props for `RealtimeDashboard`. */
export interface RealtimeDashboardProps {
status: ConnectionStatus;
stats: RealtimeDashboardStat[];
activities: ActivityDef[];
/** Top events ranked list displayed beside live feed */
topEvents?: TopEventItem[];
onLoadMore?: () => void;
/** Enable CSS animations (pulse for connection, slideIn for feed items) */
animated?: boolean;
/** Custom renderer for live event feed items */
renderEventItem?: (activity: ActivityDef) => React.ReactNode;
header?: React.ReactNode;
actions?: React.ReactNode;
labels?: RealtimeDashboardLabels;
loading?: boolean;
timeRange?: DateRange;
onTimeRangeChange?: (range: DateRange) => void;
className?: string;
}
/** Renders the `RealtimeDashboard` component. */
export function RealtimeDashboard({
status,
stats,
activities,
topEvents,
onLoadMore,
animated = false,
renderEventItem,
header,
actions,
labels: customLabels,
loading = false,
timeRange,
onTimeRangeChange,
className,
}: RealtimeDashboardProps) {
const labels = { ...defaultLabels, ...customLabels };
const titleId = React.useId();
const liveEventsId = React.useId();
const topEventsId = React.useId();
const hasTopEvents = topEvents && topEvents.length > 0;
const isEmpty =
stats.length === 0 && activities.length === 0 && !hasTopEvents;
const pulseClass = animated && status === "connected" ? "animate-pulse" : "";
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} />
<span className={pulseClass}>
<ConnectionIndicator status={status} />
</span>
{actions}
</div>
</div>
) : (
<div className="flex items-center justify-between">
<h2 id={titleId} className="text-text-primary text-lg font-semibold">
{labels.realtime}
</h2>
<div className="gap-element flex items-center">
<TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
<span className={pulseClass}>
<ConnectionIndicator status={status} />
</span>
</div>
</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.length > 0 && <SummaryRow stats={stats} columns={4} />}
{(activities.length > 0 || hasTopEvents) && (
<div
className={cn(
"gap-section grid",
hasTopEvents ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1",
)}
>
{hasTopEvents && (
<section aria-labelledby={topEventsId}>
<h3
id={topEventsId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.topEvents}
</h3>
<ul className="flex flex-col gap-1">
{topEvents.map((ev, idx) => (
<li
key={ev.id}
className={cn(
"hover:bg-bg-subtle flex items-center justify-between rounded-md px-3 py-2 text-sm",
animated &&
"animate-in fade-in slide-in-from-left-2 duration-300 [animation-fill-mode:backwards]",
)}
style={
animated
? { animationDelay: `${idx * 50}ms` }
: undefined
}
>
<span className="text-text-secondary">
<span className="text-text-tertiary mr-2 text-xs">
{idx + 1}.
</span>
{ev.name}
</span>
<span className="text-text-primary font-medium">
{ev.count.toLocaleString()}
</span>
</li>
))}
</ul>
</section>
)}
{activities.length > 0 && (
<section
aria-labelledby={liveEventsId}
className={
animated
? "[&_[data-activity-item]]:animate-in [&_[data-activity-item]]:fade-in [&_[data-activity-item]]:slide-in-from-left-2 [&_[data-activity-item]]:duration-300"
: undefined
}
>
<h3
id={liveEventsId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.liveEvents}
</h3>
{renderEventItem ? (
<div className="gap-element flex flex-col">
{activities.map((activity, i) => (
<div key={activity.id ?? i} data-activity-item>
{renderEventItem(activity)}
</div>
))}
</div>
) : (
<ActivityFeed activities={activities} />
)}
{onLoadMore && (
<button
onClick={onLoadMore}
className="text-accent mt-2 text-sm hover:underline"
>
{labels.loadMore}
</button>
)}
</section>
)}
</div>
)}
</>
)}
</SurfaceLayout>
);
}
RealtimeDashboard.displayName = "RealtimeDashboard";