SqlWorkspace
surfaceSQL 에디터 + 쿼리 결과 테이블. SqlEditor와 QueryResultsTable 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
SQL Workspace
Ctrl+Enter to run
No results
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
SqlWorkspace 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
SqlWorkspace Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
initialQuery | string | "" | 초기 SQL 쿼리 |
onRun | (sql: string) => Promise<{ columns: string[]; rows: Record<string, unknown>[] }> | — | 쿼리 실행 핸들러 |
className | string | — | 최외곽 CSS 클래스 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add sql-workspaceConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { SqlWorkspace } from "@/components/surfaces/sql-workspace";Registry metadata
- 설명
- SQL 에디터 + 쿼리 결과 테이블. SqlEditor와 QueryResultsTable 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- 없음
- Install notes
- 없음
포함 파일
sql-workspace.tsxsql-workspace.tsx
Surface 소스 보기
sql-workspace.tsx
"use client";
import * as React from "react";
import {
SqlEditor,
QueryResultsTable,
SurfaceLayout,
Button,
} from "@reopt-ai/opt-ui";
/** Data shape for `SavedQuery`. */
export interface SavedQuery {
id: string;
name: string;
sql: string;
createdAt?: string;
}
/** Data shape for `ExampleQuery`. */
export interface ExampleQuery {
label: string;
sql: string;
description?: string;
}
/** Labels for `SqlWorkspace`. */
export interface SqlWorkspaceLabels {
title?: string;
emptyTitle?: string;
emptyDescription?: string;
savedQueriesLabel?: string;
exampleQueriesLabel?: string;
saveButton?: string;
noSavedQueries?: string;
}
const defaultLabels: Required<SqlWorkspaceLabels> = {
title: "SQL Workspace",
emptyTitle: "No results",
emptyDescription: "Run a query to see results.",
savedQueriesLabel: "Saved Queries",
exampleQueriesLabel: "Examples",
saveButton: "Save",
noSavedQueries: "No saved queries",
};
/** Props for `SqlWorkspace`. */
export interface SqlWorkspaceProps {
initialQuery?: string;
onRun?: (
sql: string,
) => Promise<{ columns: string[]; rows: Array<Record<string, unknown>> }>;
/** Sidebar content (e.g., saved queries, example queries) */
sidebar?: React.ReactNode;
/** Saved query list displayed in auto-generated sidebar */
savedQueries?: SavedQuery[];
/** Callback when a saved query is selected */
onSavedQuerySelect?: (query: { id: string; sql: string }) => void;
/** Callback to save current query */
onSaveQuery?: (name: string, sql: string) => void;
/** Callback to delete a saved query */
onDeleteQuery?: (id: string) => void;
/** Example queries displayed in auto-generated sidebar */
exampleQueries?: ExampleQuery[];
loading?: boolean;
header?: React.ReactNode;
actions?: React.ReactNode;
labels?: SqlWorkspaceLabels;
className?: string;
}
/** Renders the `SqlWorkspace` component. */
export function SqlWorkspace({
initialQuery = "",
onRun,
sidebar,
savedQueries,
onSavedQuerySelect,
onSaveQuery,
onDeleteQuery,
exampleQueries,
loading: surfaceLoading = false,
header,
actions,
labels: customLabels,
className,
}: SqlWorkspaceProps) {
const labels = { ...defaultLabels, ...customLabels };
const titleId = React.useId();
const [sql, setSql] = React.useState(initialQuery);
const [columns, setColumns] = React.useState<string[]>([]);
const [rows, setRows] = React.useState<Array<Record<string, unknown>>>([]);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string>();
const handleRun = React.useCallback(
async (query: string) => {
if (!onRun) return;
setLoading(true);
setError(undefined);
try {
const result = await onRun(query);
setColumns(result.columns);
setRows(result.rows);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
},
[onRun],
);
// Auto-generate sidebar when savedQueries or exampleQueries are provided
// and no explicit sidebar prop is given (sidebar takes precedence for backward compat)
const hasSavedOrExamples =
(savedQueries && savedQueries.length > 0) ||
(exampleQueries && exampleQueries.length > 0);
const showSidebar = sidebar || hasSavedOrExamples;
const autoSidebar =
!sidebar && hasSavedOrExamples ? (
<div className="gap-group flex flex-col">
{/* Saved Queries section */}
{savedQueries !== undefined && (
<div>
<div className="mb-2 flex items-center justify-between">
<h3 className="text-text-primary text-sm font-semibold">
{labels.savedQueriesLabel}
</h3>
{onSaveQuery && (
<Button
variant="ghost"
size="sm"
onClick={() => {
const name = `Query ${(savedQueries?.length ?? 0) + 1}`;
onSaveQuery(name, sql);
}}
>
{labels.saveButton}
</Button>
)}
</div>
{savedQueries.length === 0 ? (
<p className="text-text-tertiary text-xs">
{labels.noSavedQueries}
</p>
) : (
<ul className="gap-element flex flex-col">
{savedQueries.map((q) => (
<li
key={q.id}
className="group flex items-center justify-between"
>
<button
type="button"
className="text-text-secondary hover:text-text-primary truncate text-left text-sm"
onClick={() =>
onSavedQuerySelect?.({ id: q.id, sql: q.sql })
}
>
{q.name}
</button>
{onDeleteQuery && (
<button
type="button"
className="text-text-tertiary hover:text-danger ml-1 shrink-0 text-xs opacity-0 group-hover:opacity-100"
onClick={() => onDeleteQuery(q.id)}
aria-label={`Delete ${q.name}`}
>
✕
</button>
)}
</li>
))}
</ul>
)}
</div>
)}
{/* Example Queries section */}
{exampleQueries && exampleQueries.length > 0 && (
<div>
<h3 className="text-text-primary mb-2 text-sm font-semibold">
{labels.exampleQueriesLabel}
</h3>
<ul className="gap-element flex flex-col">
{exampleQueries.map((eq, i) => (
<li key={i}>
<button
type="button"
className="text-text-secondary hover:text-text-primary w-full text-left text-sm"
title={eq.description}
onClick={() => {
setSql(eq.sql);
onSavedQuerySelect?.({ id: `example-${i}`, sql: eq.sql });
}}
>
{eq.label}
</button>
</li>
))}
</ul>
</div>
)}
</div>
) : null;
const sidebarContent = sidebar || autoSidebar;
return (
<SurfaceLayout loading={surfaceLoading} 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.title}
</h2>
)}
{showSidebar ? (
<div className="gap-section flex">
<aside className="border-border w-64 shrink-0 overflow-y-auto border-r pr-4">
{sidebarContent}
</aside>
<div className="gap-section flex min-w-0 flex-1 flex-col">
<SqlEditor value={sql} onChange={setSql} onRun={handleRun} />
<QueryResultsTable
columns={columns}
rows={rows}
loading={loading}
error={error}
/>
</div>
</div>
) : (
<>
<SqlEditor value={sql} onChange={setSql} onRun={handleRun} />
<QueryResultsTable
columns={columns}
rows={rows}
loading={loading}
error={error}
/>
</>
)}
</SurfaceLayout>
);
}
SqlWorkspace.displayName = "SqlWorkspace";