ProjectSettings
surface프로젝트 설정 Surface. SettingsForm + DataTable + CodeSnippetViewer + Retention Policy + Cleanup Preview + Storage Usage 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Project Settings
Project Settings
API Keys
Data Table
| Name | API Key | Created |
|---|---|---|
| Production | pk_live_abc123... | 2024-01-15 |
| Development | pk_test_xyz789... | 2024-02-01 |
SDK Integration
Installation
1npm install @reopt-ai/sdk\n\nimport { init } from '@reopt-ai/sdk';\ninit({ apiKey: 'pk_live_abc123' });테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
ProjectSettings 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
ProjectSettings Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
formFields* | FormFieldDef[] | — | 폼 필드 정의 배열 |
onFormSubmit | (values: Record<string, unknown>) => void | — | 폼 제출 핸들러 |
apiKeys | ApiKeyRow[] | — | API 키 배열 |
sdkSnippet | string | — | SDK 설치 코드 스니펫 |
retentionPolicy | RetentionPolicyDef | — | 데이터 보존 정책 옵션 |
onRetentionPolicyChange | (policyId: string) => void | — | 보존 정책 변경 핸들러 |
cleanupPreview | CleanupPreviewRow[] | — | 클린업 미리보기 데이터 |
onCleanup | () => void | — | 클린업 실행 핸들러 |
cleanupConfirmRequired | boolean | true | 클린업 확인 다이얼로그 필요 여부 |
storageUsage | StorageUsageRow[] | — | 스토리지 사용량 데이터 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add project-settingsConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { ProjectSettings } from "@/components/surfaces/project-settings";Registry metadata
- 설명
- 프로젝트 설정 Surface. SettingsForm + DataTable + CodeSnippetViewer + Retention Policy + Cleanup Preview + Storage Usage 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- formtablesettings
- Install notes
- 없음
포함 파일
project-settings.tsxproject-settings.tsx
Surface 소스 보기
project-settings.tsx
"use client";
import * as React from "react";
import {
Button,
Progress,
SettingsForm,
DataTable,
CodeSnippetViewer,
ConfirmDialog,
SurfaceLayout,
type FormFieldDef,
type ColumnDef,
} from "@reopt-ai/opt-ui";
interface ApiKeyRow {
name: string;
key: string;
created: string;
}
/* ------------------------------------------------------------------ */
/* Retention / Cleanup / Storage types */
/* ------------------------------------------------------------------ */
/** Option shape for `RetentionPolicy`. */
export interface RetentionPolicyOption {
id: string;
label: string;
description?: string;
}
/** Definition shape for `RetentionPolicy`. */
export interface RetentionPolicyDef {
current: string;
options: RetentionPolicyOption[];
}
/** Row shape for `CleanupPreview`. */
export interface CleanupPreviewRow {
table: string;
rows: number;
size: string;
}
/** Row shape for `StorageUsage`. */
export interface StorageUsageRow {
table: string;
rows: number;
size: string;
percentage: number;
}
/* ------------------------------------------------------------------ */
/* Labels */
/* ------------------------------------------------------------------ */
/** Labels for `ProjectSettings`. */
export interface ProjectSettingsLabels {
settings?: string;
apiKeys?: string;
sdk?: string;
installation?: string;
retentionTitle?: string;
retentionDescription?: string;
cleanupTitle?: string;
cleanupButton?: string;
cleanupWarning?: string;
cleanupConfirmTitle?: string;
cleanupConfirmDescription?: string;
cleanupConfirmButton?: string;
cancelButton?: string;
storageTitle?: string;
emptyTitle?: string;
emptyDescription?: string;
}
const defaultLabels: Required<ProjectSettingsLabels> = {
settings: "Project Settings",
apiKeys: "API Keys",
sdk: "SDK Integration",
installation: "Installation",
retentionTitle: "Data Retention Policy",
retentionDescription:
"Choose how long to keep your project data before automatic cleanup.",
cleanupTitle: "Data Cleanup Preview",
cleanupButton: "Run Cleanup",
cleanupWarning:
"Running cleanup will permanently delete the data listed below. This action cannot be undone.",
cleanupConfirmTitle: "Confirm Data Cleanup",
cleanupConfirmDescription:
"Are you sure you want to permanently delete the data listed above? This action cannot be undone.",
cleanupConfirmButton: "Run Cleanup",
cancelButton: "Cancel",
storageTitle: "Storage Usage",
emptyTitle: "No settings configured",
emptyDescription: "Project settings will appear here once configured.",
};
/* ------------------------------------------------------------------ */
/* Props */
/* ------------------------------------------------------------------ */
/** Props for `ProjectSettings`. */
export interface ProjectSettingsProps {
formFields: FormFieldDef[];
onFormSubmit?: (values: Record<string, unknown>) => void;
apiKeys?: ApiKeyRow[];
apiKeyColumns?: ColumnDef<ApiKeyRow>[];
sdkSnippet?: string;
sdkLanguage?: string;
/** Retention policy configuration */
retentionPolicy?: RetentionPolicyDef;
/** Retention policy change handler */
onRetentionPolicyChange?: (policyId: string) => void;
/** Cleanup preview data */
cleanupPreview?: CleanupPreviewRow[];
/** Manual cleanup trigger */
onCleanup?: () => void;
/** Require confirmation for cleanup (default: true) */
cleanupConfirmRequired?: boolean;
/** Storage usage breakdown */
storageUsage?: StorageUsageRow[];
/** Extra sections rendered after all built-in sections */
extraSections?: React.ReactNode;
header?: React.ReactNode;
actions?: React.ReactNode;
labels?: ProjectSettingsLabels;
className?: string;
loading?: boolean;
}
/* ------------------------------------------------------------------ */
/* Default columns */
/* ------------------------------------------------------------------ */
const defaultApiKeyColumns: ColumnDef<ApiKeyRow>[] = [
{ id: "name", header: "Name", accessor: "name" },
{ id: "key", header: "API Key", accessor: "key" },
{ id: "created", header: "Created", accessor: "created" },
];
const cleanupPreviewColumns: ColumnDef<CleanupPreviewRow>[] = [
{ id: "table", header: "Table", accessor: "table" },
{
id: "rows",
header: "Rows",
accessor: (row) => row.rows.toLocaleString(),
},
{ id: "size", header: "Size", accessor: "size" },
];
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
/** Renders the `ProjectSettings` component. */
export function ProjectSettings({
formFields,
onFormSubmit,
apiKeys = [],
apiKeyColumns = defaultApiKeyColumns,
sdkSnippet,
sdkLanguage = "typescript",
retentionPolicy,
onRetentionPolicyChange,
cleanupPreview,
onCleanup,
cleanupConfirmRequired = true,
storageUsage,
extraSections,
header,
actions,
labels: customLabels,
className,
loading = false,
}: ProjectSettingsProps) {
const labels = { ...defaultLabels, ...customLabels };
const titleId = React.useId();
const settingsId = React.useId();
const apiKeysId = React.useId();
const sdkId = React.useId();
const retentionId = React.useId();
const cleanupId = React.useId();
const storageId = React.useId();
const [showCleanupConfirm, setShowCleanupConfirm] = React.useState(false);
const isEmpty = formFields.length === 0;
const handleCleanup = () => {
if (cleanupConfirmRequired) {
setShowCleanupConfirm(true);
} else {
onCleanup?.();
}
};
const handleCleanupConfirm = () => {
setShowCleanupConfirm(false);
onCleanup?.();
};
const storageColumns = React.useMemo<ColumnDef<StorageUsageRow>[]>(
() => [
{ id: "table", header: "Table", accessor: "table" },
{
id: "rows",
header: "Rows",
accessor: (row) => row.rows.toLocaleString(),
},
{ id: "size", header: "Size", accessor: "size" },
{
id: "usage",
header: "Usage",
accessor: () => "",
cell: (row: StorageUsageRow) => (
<div className="gap-element flex items-center">
<Progress
value={row.percentage}
max={100}
size="sm"
variant={
row.percentage >= 90
? "error"
: row.percentage >= 70
? "warning"
: "default"
}
className="w-20"
/>
<span className="text-text-tertiary text-xs">
{row.percentage}%
</span>
</div>
),
},
],
[],
);
const hasRetention = retentionPolicy && retentionPolicy.options.length > 0;
const hasCleanup = cleanupPreview && cleanupPreview.length > 0;
const hasStorage = storageUsage && storageUsage.length > 0;
return (
<SurfaceLayout loading={loading} className={className}>
{header || actions ? (
<div className="flex items-center justify-between">
{header && <div>{header}</div>}
{actions && (
<div className="gap-element flex items-center">{actions}</div>
)}
</div>
) : (
<h2 id={titleId} className="text-text-primary text-lg font-semibold">
{labels.settings}
</h2>
)}
{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>
) : (
<>
<section aria-labelledby={settingsId}>
<h3
id={settingsId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.settings}
</h3>
<SettingsForm fields={formFields} onSubmit={onFormSubmit} />
</section>
{apiKeys.length > 0 && (
<section aria-labelledby={apiKeysId}>
<h3
id={apiKeysId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.apiKeys}
</h3>
<DataTable
columns={apiKeyColumns}
data={apiKeys}
keyExtractor={(r) => r.key}
/>
</section>
)}
{sdkSnippet && (
<section aria-labelledby={sdkId}>
<h3
id={sdkId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.sdk}
</h3>
<CodeSnippetViewer
code={sdkSnippet}
language={sdkLanguage}
title={labels.installation}
/>
</section>
)}
{/* ---- Retention Policy Section ---- */}
{hasRetention && (
<section aria-labelledby={retentionId}>
<h3
id={retentionId}
className="text-text-tertiary mb-1 text-sm font-medium"
>
{labels.retentionTitle}
</h3>
<p className="text-text-tertiary mb-3 text-xs">
{labels.retentionDescription}
</p>
<div
className="gap-element flex flex-col"
role="radiogroup"
aria-labelledby={retentionId}
>
{retentionPolicy.options.map((option) => {
const isSelected = retentionPolicy.current === option.id;
return (
<button
key={option.id}
type="button"
role="radio"
aria-checked={isSelected}
onClick={() => onRetentionPolicyChange?.(option.id)}
className={`gap-group flex items-start rounded-lg border p-3 text-left transition-colors ${
isSelected
? "border-accent bg-accent/5"
: "border-border-subtle hover:bg-bg-subtle"
}`}
>
<span
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${
isSelected ? "border-accent" : "border-border"
}`}
>
{isSelected && (
<span className="bg-accent h-2 w-2 rounded-full" />
)}
</span>
<div className="flex flex-col">
<span className="text-text-primary text-sm font-medium">
{option.label}
</span>
{option.description && (
<span className="text-text-tertiary text-xs">
{option.description}
</span>
)}
</div>
</button>
);
})}
</div>
</section>
)}
{/* ---- Cleanup Preview Section ---- */}
{hasCleanup && (
<section aria-labelledby={cleanupId}>
<h3
id={cleanupId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.cleanupTitle}
</h3>
<DataTable
columns={cleanupPreviewColumns}
data={cleanupPreview}
keyExtractor={(r) => r.table}
/>
{onCleanup && (
<div className="mt-3">
<p className="text-text-tertiary mb-2 text-xs">
{labels.cleanupWarning}
</p>
<Button variant="danger" size="sm" onClick={handleCleanup}>
{labels.cleanupButton}
</Button>
</div>
)}
</section>
)}
{/* ---- Storage Usage Section ---- */}
{hasStorage && (
<section aria-labelledby={storageId}>
<h3
id={storageId}
className="text-text-tertiary mb-2 text-sm font-medium"
>
{labels.storageTitle}
</h3>
<DataTable
columns={storageColumns}
data={storageUsage}
keyExtractor={(r) => r.table}
/>
</section>
)}
{extraSections}
</>
)}
{/* ---- Cleanup Confirmation Dialog ---- */}
<ConfirmDialog
open={showCleanupConfirm}
onConfirm={handleCleanupConfirm}
onCancel={() => setShowCleanupConfirm(false)}
title={labels.cleanupConfirmTitle}
description={labels.cleanupConfirmDescription}
confirmLabel={labels.cleanupConfirmButton}
cancelLabel={labels.cancelButton}
variant="danger"
/>
</SurfaceLayout>
);
}
ProjectSettings.displayName = "ProjectSettings";