InsightsDashboard
surface인사이트 대시보드 Surface. InsightsPanel 래핑 + 탐지 실행, 시간 범위 필터, 에러 표시, dismissed 토글, 탐지 메서드/파라미터 제어.
컴포넌트 의존 관계
깊이
100%
기본 사용
Insights
Show dismissed
Anomaly Detection: Detect statistical anomalies in metric trendsThreshold Alerts: Alert when metrics cross configured thresholdsTrend Analysis: Identify long-term trends and seasonal patterns
Detection Settings
Signup conversion dropcritical
Signup-to-onboarding conversion dropped 18% compared to the previous 7 days.
conversion_rate: -18%
Session duration spikewarning
Average session duration increased by 32% on mobile devices.
session_duration: +32%
API error rate normalinfo
API error rate returned to baseline after yesterday's spike.
error_rate: 0.3%
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
InsightsDashboard 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
InsightsDashboard Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
insights* | InsightItem[] | — | 인사이트 목록 |
stats | InsightsStat[] | — | 인사이트 통계 카드 |
detectionTypes | DetectionTypeDef[] | — | 탐지 유형 토글 |
onDetectionTypeToggle | (typeId: string, enabled: boolean) => void | — | 탐지 유형 변경 핸들러 |
detectionConfig | DetectionConfig | — | 탐지 파라미터 (days, z-score) |
onDetectionConfigChange | (config: DetectionConfig) => void | — | 탐지 파라미터 변경 핸들러 |
detections | Array<{ id: string; name: string; enabled: boolean; description?: string }> | — | 탐지 메서드 정의 (detectionTypes 별칭, description 포함) |
onDetectionToggle | (detectionId: string, enabled: boolean) => void | — | 탐지 메서드 토글 (onDetectionTypeToggle 별칭) |
detectionParams | Record<string, number> | — | 탐지 파라미터 key-value (detectionConfig 별칭) |
onDetectionParamChange | (key: string, value: number) => void | — | 탐지 파라미터 변경 핸들러 (onDetectionConfigChange 별칭) |
showDismissed | boolean | — | dismissed 인사이트 표시 여부 (제어 모드) |
onShowDismissedChange | (show: boolean) => void | — | dismissed 토글 변경 핸들러 |
onDismiss | (id: string) => void | — | 인사이트 무시 핸들러 |
onDismissInsight | (insightId: string) => void | — | 인사이트 무시 핸들러 (onDismiss 별칭) |
onInsightClick | (insight: InsightItem) => void | — | 인사이트 클릭 핸들러 |
onStatClick | (statId: string) => void | — | 통계 카드 클릭 핸들러 |
onRun | () => void | — | 탐지 실행 핸들러 |
running | boolean | — | 탐지 실행 중 상태 |
error | string | — | 에러 메시지 |
timeRange | DateRange | — | 시간 범위 필터 |
onTimeRangeChange | (range: DateRange) => void | — | 시간 범위 변경 핸들러 |
loading | boolean | — | 로딩 상태 |
header | ReactNode | — | 커스텀 헤더 슬롯 |
actions | ReactNode | — | 액션 버튼 슬롯 |
labels | InsightsDashboardLabels | — | 커스텀 레이블 |
className | string | — | 최외곽 CSS 클래스 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add insights-dashboardConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { InsightsDashboard } from "@/components/surfaces/insights-dashboard";Registry metadata
- 설명
- 인사이트 대시보드 Surface. InsightsPanel 래핑 + 탐지 실행, 시간 범위 필터, 에러 표시, dismissed 토글, 탐지 메서드/파라미터 제어.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- dashboard
- Install notes
- 없음
포함 파일
insights-dashboard.tsxinsights-dashboard.tsx
Surface 소스 보기
insights-dashboard.tsx
"use client";
import * as React from "react";
import {
InsightsPanel,
Button,
Alert,
Switch,
SurfaceLayout,
TimeRangeSelector,
type InsightItem,
type InsightsStat,
type DetectionTypeDef,
type DetectionConfig,
type InsightsPanelLabels,
type DateRange,
} from "@reopt-ai/opt-ui";
/** Labels for `InsightsDashboard`. */
export interface InsightsDashboardLabels extends InsightsPanelLabels {
dashboard?: string;
runDetection?: string;
running?: string;
emptyTitle?: string;
emptyDescription?: string;
emptyRunAction?: string;
/** Label for the show dismissed toggle at surface level */
showDismissedLabel?: string;
/** Title for the detection settings panel */
detectionPanelTitle?: string;
/** Label for dismiss insight action */
dismissButton?: string;
}
const defaultLabels: Required<InsightsDashboardLabels> = {
dashboard: "Insights",
runDetection: "Run Detection",
running: "Running...",
emptyTitle: "No insights yet",
emptyDescription: "Run anomaly detection to discover insights.",
emptyRunAction: "Run Detection",
showDismissedLabel: "Show dismissed",
detectionPanelTitle: "Detection Settings",
dismissButton: "Dismiss",
title: "",
dismiss: "Dismiss",
showDismissed: "Show dismissed",
hideDismissed: "Hide dismissed",
detectionTitle: "Detection Settings",
daysLabel: "Days",
zScoreLabel: "Z-Score",
};
/** Props for `InsightsDashboard`. */
export interface InsightsDashboardProps {
insights: InsightItem[];
stats?: InsightsStat[];
detectionTypes?: DetectionTypeDef[];
onDetectionTypeToggle?: (typeId: string, enabled: boolean) => void;
detectionConfig?: DetectionConfig;
onDetectionConfigChange?: (config: DetectionConfig) => void;
onDismiss?: (id: string) => void;
onInsightClick?: (insight: InsightItem) => void;
onStatClick?: (statId: string) => void;
/** Trigger detection run */
onRun?: () => void;
/** Whether detection is currently running */
running?: boolean;
/** Error message to display */
error?: string;
/** Controlled show/hide dismissed insights toggle */
showDismissed?: boolean;
/** Callback when show dismissed toggle changes */
onShowDismissedChange?: (show: boolean) => void;
/** Alias for onDismiss — dismiss a specific insight */
onDismissInsight?: (insightId: string) => void;
/** Detection method definitions (alias for detectionTypes) */
detections?: Array<{
id: string;
name: string;
enabled: boolean;
description?: string;
}>;
/** Toggle a detection method (alias for onDetectionTypeToggle) */
onDetectionToggle?: (detectionId: string, enabled: boolean) => void;
/** Detection parameters as key-value record */
detectionParams?: Record<string, number>;
/** Callback when a detection parameter changes */
onDetectionParamChange?: (key: string, value: number) => void;
loading?: boolean;
timeRange?: DateRange;
onTimeRangeChange?: (range: DateRange) => void;
header?: React.ReactNode;
actions?: React.ReactNode;
labels?: InsightsDashboardLabels;
className?: string;
}
/** Renders the `InsightsDashboard` component. */
export function InsightsDashboard({
insights,
stats,
detectionTypes,
onDetectionTypeToggle,
detectionConfig,
onDetectionConfigChange,
onDismiss,
onInsightClick,
onStatClick,
onRun,
running = false,
error,
showDismissed: controlledShowDismissed,
onShowDismissedChange,
onDismissInsight,
detections,
onDetectionToggle,
detectionParams,
onDetectionParamChange,
loading = false,
timeRange,
onTimeRangeChange,
header,
actions,
labels: customLabels,
className,
}: InsightsDashboardProps) {
const labels = { ...defaultLabels, ...customLabels };
const titleId = React.useId();
// Merge detections alias into detectionTypes
const mergedDetectionTypes: DetectionTypeDef[] | undefined =
detectionTypes ??
detections?.map((d) => ({ id: d.id, label: d.name, enabled: d.enabled }));
const mergedDetectionToggle = onDetectionTypeToggle ?? onDetectionToggle;
// Merge detectionParams into detectionConfig
const mergedDetectionConfig: DetectionConfig | undefined =
detectionConfig ??
(detectionParams
? {
days: detectionParams.days,
zScoreThreshold: detectionParams.zScoreThreshold,
}
: undefined);
const mergedDetectionConfigChange =
onDetectionConfigChange ??
(onDetectionParamChange
? (config: DetectionConfig) => {
if (config.days != null) onDetectionParamChange("days", config.days);
if (config.zScoreThreshold != null)
onDetectionParamChange("zScoreThreshold", config.zScoreThreshold);
}
: undefined);
// Merge dismiss callback
const mergedDismiss = onDismiss ?? onDismissInsight;
// Controlled vs uncontrolled showDismissed
const [internalShowDismissed, setInternalShowDismissed] =
React.useState(false);
const isControlled = controlledShowDismissed !== undefined;
const showDismissed = isControlled
? controlledShowDismissed
: internalShowDismissed;
const toggleShowDismissed = () => {
const next = !showDismissed;
if (onShowDismissedChange) {
onShowDismissedChange(next);
}
if (!isControlled) {
setInternalShowDismissed(next);
}
};
const isEmpty =
insights.length === 0 &&
(!stats || stats.length === 0) &&
(!mergedDetectionTypes || mergedDetectionTypes.length === 0) &&
mergedDetectionConfig == null;
const runButton = onRun ? (
<Button variant="primary" size="sm" onClick={onRun} disabled={running}>
{running ? labels.running : labels.runDetection}
</Button>
) : null;
const panelLabels: InsightsPanelLabels = {
title: labels.title,
dismiss: labels.dismissButton || labels.dismiss,
showDismissed: labels.showDismissed,
hideDismissed: labels.hideDismissed,
emptyTitle: labels.emptyTitle,
emptyDescription: labels.emptyDescription,
detectionTitle: labels.detectionPanelTitle || labels.detectionTitle,
daysLabel: labels.daysLabel,
zScoreLabel: labels.zScoreLabel,
};
// Filter insights based on showDismissed state at surface level
const visibleInsights = showDismissed
? insights
: insights.filter((i) => !i.dismissed);
return (
<SurfaceLayout loading={loading} className={className}>
<div className="flex items-center justify-between">
{header || actions ? (
<>
{header && <div>{header}</div>}
<div className="gap-element flex items-center">
<TimeRangeSelector
value={timeRange}
onChange={onTimeRangeChange}
/>
{actions}
{runButton}
</div>
</>
) : (
<>
<h2
id={titleId}
className="text-text-primary text-lg font-semibold"
>
{labels.dashboard}
</h2>
<div className="gap-element flex items-center">
<TimeRangeSelector
value={timeRange}
onChange={onTimeRangeChange}
/>
{runButton}
</div>
</>
)}
</div>
{/* Show dismissed toggle (controlled or uncontrolled at surface level) */}
{(onShowDismissedChange || !isControlled) && insights.length > 0 && (
<div className="gap-element flex items-center">
<Switch
checked={showDismissed}
onChange={toggleShowDismissed}
size="sm"
/>
<span className="text-text-secondary text-sm">
{labels.showDismissedLabel}
</span>
</div>
)}
{/* Detection descriptions (from detections alias) */}
{detections &&
detections.some((d) => d.description) &&
!detectionTypes && (
<div className="gap-element flex flex-wrap">
{detections
.filter((d) => d.description)
.map((d) => (
<span
key={d.id}
className="text-text-tertiary text-xs"
title={d.description}
>
<span className="font-medium">{d.name}:</span> {d.description}
</span>
))}
</div>
)}
{error && (
<Alert variant="error" aria-live="polite">
{error}
</Alert>
)}
{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>
{onRun && (
<Button
variant="primary"
size="sm"
className="mt-4"
onClick={onRun}
disabled={running}
>
{running ? labels.running : labels.emptyRunAction}
</Button>
)}
</div>
) : (
<InsightsPanel
insights={visibleInsights}
stats={stats}
detectionTypes={mergedDetectionTypes}
onDetectionTypeToggle={mergedDetectionToggle}
detectionConfig={mergedDetectionConfig}
onDetectionConfigChange={mergedDetectionConfigChange}
onDismiss={mergedDismiss}
onInsightClick={onInsightClick}
onStatClick={onStatClick}
labels={panelLabels}
/>
)}
</SurfaceLayout>
);
}
InsightsDashboard.displayName = "InsightsDashboard";