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.

AlertManager

surface

알림 관리 Surface. DataTable + AlertBuilder 조합.

컴포넌트 의존 관계

깊이
▼ USES (4)AlertManagerdata-tablealert-builderloading-overlaybutton
100%

기본 사용

Alerts

Data Table

NameFrequencyChannelsStatus
High Error Raterealtimeemail, slackActive
Low ConversionhourlyemailInactive

테스트 커버리지

2026년 2월 4일

생성된 테스트 결과를 찾지 못했습니다.

AlertManager 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.

테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.

AlertManager Props

Prop타입기본값설명
alerts*AlertDef[]—알림 정의 배열
onChange*(alerts: AlertDef[]) => void—알림 변경 핸들러
classNamestring—최외곽 CSS 클래스

Surface 설치

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

bash
npx @reopt-ai/opt-cli surface add alert-manager

Consumer target

복사된 파일은 components/surfaces 아래에 저장됩니다.

tsx
import { AlertManager } from "@/components/surfaces/alert-manager";

Registry metadata

설명
알림 관리 Surface. DataTable + AlertBuilder 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
table
Install notes
없음

포함 파일

  • alert-manager.tsx→alert-manager.tsx
Surface 소스 보기
alert-manager.tsx
"use client";

import * as React from "react";
import {
  DataTable,
  AlertBuilder,
  Button,
  Spinner,
  SurfaceLayout,
  type AlertConfig,
  type ColumnDef,
} from "@reopt-ai/opt-ui";

/** Labels for `AlertManager`. */
export interface AlertManagerLabels {
  alerts?: string;
  newAlert?: string;
  editAlert?: string;
  deleteButton?: string;
  testButton?: string;
  testingLabel?: string;
  emptyTitle?: string;
  emptyDescription?: string;
}

const defaultLabels: Required<AlertManagerLabels> = {
  alerts: "Alerts",
  newAlert: "+ New Alert",
  editAlert: "Edit Alert",
  deleteButton: "Delete",
  testButton: "Test",
  testingLabel: "Testing…",
  emptyTitle: "No alerts configured",
  emptyDescription: "Create your first alert to start monitoring.",
};

/** Props for `AlertManager`. */
export interface AlertManagerProps {
  alerts: AlertConfig[];
  onChange: (alerts: AlertConfig[]) => void;
  /** Callback to test the selected alert's conditions. When provided, a "Test" button appears in the edit panel. */
  onTestCondition?: (alert: AlertConfig) => void;
  /** Name of the alert currently being tested (shows a spinner). */
  testingAlertName?: string | null;
  /** Dynamic event options for the condition builder event selector. */
  eventOptions?: { id: string; name: string }[];
  loading?: boolean;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  labels?: AlertManagerLabels;
  className?: string;
}

const columns: ColumnDef<AlertConfig>[] = [
  { id: "name", header: "Name", accessor: "name" },
  { id: "frequency", header: "Frequency", accessor: "frequency" },
  {
    id: "channels",
    header: "Channels",
    accessor: (row) => row.channels.join(", "),
  },
  {
    id: "enabled",
    header: "Status",
    accessor: (row) => (row.enabled ? "Active" : "Inactive"),
  },
];

/** Renders the `AlertManager` component. */
export function AlertManager({
  alerts,
  onChange,
  onTestCondition,
  testingAlertName = null,
  eventOptions,
  loading = false,
  header,
  actions,
  labels: customLabels,
  className,
}: AlertManagerProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const titleId = React.useId();

  const [editing, setEditing] = React.useState<number | null>(null);

  const addAlert = () => {
    const newAlert: AlertConfig = {
      name: "New Alert",
      conditions: [],
      channels: [],
      frequency: "realtime",
      enabled: true,
    };
    onChange([...alerts, newAlert]);
    setEditing(alerts.length);
  };

  const updateAlert = (config: AlertConfig) => {
    if (editing === null) return;
    const next = alerts.map((a, i) => (i === editing ? config : a));
    onChange(next);
  };

  const deleteAlert = (index: number) => {
    onChange(alerts.filter((_, i) => i !== index));
    if (editing === index) setEditing(null);
  };

  const isEmpty = alerts.length === 0;

  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">
              {actions}
              <Button variant="primary" size="sm" onClick={addAlert}>
                {labels.newAlert}
              </Button>
            </div>
          </>
        ) : (
          <>
            <h2
              id={titleId}
              className="text-text-primary text-lg font-semibold"
            >
              {labels.alerts}
            </h2>
            <Button variant="primary" size="sm" onClick={addAlert}>
              {labels.newAlert}
            </Button>
          </>
        )}
      </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>
      ) : (
        <div className="gap-group flex">
          <div className="flex-1">
            <DataTable
              columns={columns}
              data={alerts}
              keyExtractor={(a) => a.name}
              onRowClick={(row) => setEditing(alerts.indexOf(row))}
            />
          </div>
          {editing !== null && alerts[editing] && (
            <div className="border-border w-80 shrink-0 border-l pl-4">
              <div className="mb-3 flex items-center justify-between">
                <h3 className="text-text-primary text-sm font-medium">
                  {labels.editAlert}
                </h3>
                <button
                  onClick={() => deleteAlert(editing)}
                  className="text-danger text-xs"
                >
                  {labels.deleteButton}
                </button>
              </div>
              <AlertBuilder
                value={alerts[editing]}
                onChange={updateAlert}
                eventOptions={eventOptions}
              />
              {onTestCondition && (
                <div className="mt-3">
                  <Button
                    variant="secondary"
                    size="sm"
                    onClick={() => onTestCondition(alerts[editing])}
                    disabled={testingAlertName === alerts[editing].name}
                  >
                    {testingAlertName === alerts[editing].name ? (
                      <span className="flex items-center gap-1.5">
                        <Spinner size="sm" />
                        {labels.testingLabel}
                      </span>
                    ) : (
                      labels.testButton
                    )}
                  </Button>
                </div>
              )}
            </div>
          )}
        </div>
      )}
    </SurfaceLayout>
  );
}

AlertManager.displayName = "AlertManager";