AlertManager
surface알림 관리 Surface. DataTable + AlertBuilder 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Alerts
Data Table
| Name | Frequency | Channels | Status |
|---|---|---|---|
| High Error Rate | realtime | email, slack | Active |
| Low Conversion | hourly | Inactive |
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
AlertManager 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
AlertManager Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
alerts* | AlertDef[] | — | 알림 정의 배열 |
onChange* | (alerts: AlertDef[]) => void | — | 알림 변경 핸들러 |
className | string | — | 최외곽 CSS 클래스 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add alert-managerConsumer 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.tsxalert-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";