reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

InsightsDashboard

surface

인사이트 대시보드 Surface. InsightsPanel 래핑 + 탐지 실행, 시간 범위 필터, 에러 표시, dismissed 토글, 탐지 메서드/파라미터 제어.

컴포넌트 의존 관계

깊이
▼ USES (4)InsightsDashboardinsights-panelswitchtime-range-selectorloading-overlay
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%
i
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[]—인사이트 목록
statsInsightsStat[]—인사이트 통계 카드
detectionTypesDetectionTypeDef[]—탐지 유형 토글
onDetectionTypeToggle(typeId: string, enabled: boolean) => void—탐지 유형 변경 핸들러
detectionConfigDetectionConfig—탐지 파라미터 (days, z-score)
onDetectionConfigChange(config: DetectionConfig) => void—탐지 파라미터 변경 핸들러
detectionsArray<{ id: string; name: string; enabled: boolean; description?: string }>—탐지 메서드 정의 (detectionTypes 별칭, description 포함)
onDetectionToggle(detectionId: string, enabled: boolean) => void—탐지 메서드 토글 (onDetectionTypeToggle 별칭)
detectionParamsRecord<string, number>—탐지 파라미터 key-value (detectionConfig 별칭)
onDetectionParamChange(key: string, value: number) => void—탐지 파라미터 변경 핸들러 (onDetectionConfigChange 별칭)
showDismissedboolean—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—탐지 실행 핸들러
runningboolean—탐지 실행 중 상태
errorstring—에러 메시지
timeRangeDateRange—시간 범위 필터
onTimeRangeChange(range: DateRange) => void—시간 범위 변경 핸들러
loadingboolean—로딩 상태
headerReactNode—커스텀 헤더 슬롯
actionsReactNode—액션 버튼 슬롯
labelsInsightsDashboardLabels—커스텀 레이블
classNamestring—최외곽 CSS 클래스

Surface 설치

CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.

bash
npx @reopt-ai/opt-cli surface add insights-dashboard

Consumer 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.tsx→insights-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";