Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions src/renderer/src/components/workspace/query-panel/query-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { useState } from 'react'
interface QueryEditorProps {
query: string
setQuery: (query: string) => void
}

/**
* @author nahyeongjin1
* @summary 쿼리 편집기 패널
* @returns JSX.Element
*/
export default function QueryEditor(): React.JSX.Element {
const [query, setQuery] = useState(
'SELECT p.ProductName, SUM(sod.sales_quantity) as total_quantity_sold, ' +
'SUM(sod.sales_quantity * sod.UnitPrice) as total_revenue ' +
'FROM Products p ' +
'JOIN SalesOrderDetails sod ON p.ProductID = sod.ProductID ' +
'GROUP BY p.ProductID, p.ProductName ' +
'ORDER BY total_revenue DESC ' +
'LIMIT 5;'
)

export default function QueryEditor({ query, setQuery }: QueryEditorProps): React.JSX.Element {
return (
<div className="h-full p-5">
<textarea
Expand Down
83 changes: 73 additions & 10 deletions src/renderer/src/components/workspace/query-panel/query-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
import React, { useState } from 'react'
import { Code2, ChartColumn, Download } from 'lucide-react'
import { Code2, ChartColumn, Download, Play } from 'lucide-react'
import { cn } from '@/lib/utils'
import QueryEditor from './query-editor'
import QueryResults from './query-results'
import QueryResults, { type QueryResultData } from './query-results'

type ActiveTab = 'editor' | 'results'

// Mock API function
const executeQuery = async (query: string): Promise<QueryResultData> => {
console.log('Executing query:', query)
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate success/error randomly
if (Math.random() > 0.3) {
resolve({
columns: ['id', 'name', 'email', 'age'],
rows: [
[1, 'Alice', 'alice@example.com', 30],
[2, 'Bob', 'bob@example.com', 25],
[3, 'Charlie', 'charlie@example.com', 35],
[4, 'David', 'david@example.com', 28]
]
})
} else {
reject(new Error("Syntax error near 'FROM'. Check your SQL syntax."))
}
}, 1500)
})
}

/**
* @author nahyeongjin1
* @summary 쿼리 편집기 및 결과 탭 패널
* @returns JSX.Element
*/
export default function QueryPanel(): React.JSX.Element {
const [activeTab, setActiveTab] = useState<ActiveTab>('editor')
const [query, setQuery] = useState(
'SELECT p.ProductName, SUM(sod.sales_quantity) as total_quantity_sold, SUM(sod.sales_quantity * sod.UnitPrice) as total_revenue FROM Products p JOIN SalesOrderDetails sod ON p.ProductID = sod.ProductID GROUP BY p.ProductID, p.ProductName ORDER BY total_revenue DESC LIMIT 5;'
)
const [result, setResult] = useState<QueryResultData | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const handleExecuteQuery = async (): Promise<void> => {
setIsLoading(true)
setError(null)
try {
const queryResult = await executeQuery(query)
setResult(queryResult)
setActiveTab('results')
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred.')
setResult(null)
setActiveTab('results')
} finally {
setIsLoading(false)
}
}

const TabButton = ({
tabName,
Expand All @@ -39,22 +84,40 @@ export default function QueryPanel(): React.JSX.Element {

return (
<div className="flex-1 h-full flex flex-col bg-neutral-800 outline-1 outline-offset-[-1px] outline-neutral-700">
{/* Tab Header */}
<div className="flex justify-between items-center border-b border-neutral-700 pr-3 pl-4">
<div className="flex gap-6 px-1">
<TabButton tabName="editor" Icon={Code2} label="쿼리 편집기" />
<TabButton tabName="results" Icon={ChartColumn} label="실행 결과" />
</div>
<div className="px-3 py-1.5 bg-gradient-genie-primary rounded-lg outline-1 outline-offset-[-1px] outline-white/20 flex justify-center items-center gap-2 cursor-pointer">
<Download className="size-3 stroke-genie-100" />
<div className="justify-start text-genie-100 text-button font-pretendard">내보내기</div>
<div className="flex items-center gap-2">
{activeTab == 'editor' ? (
<button
onClick={handleExecuteQuery}
disabled={isLoading}
className={cn(
'flex items-center gap-2 bg-gradient-genie-gray rounded-lg px-3 py-1.5 outline-1 outline-white/20 outline-offset-[-1px]',
isLoading ? '' : 'cursor-pointer'
)}
>
<Play className="size-4 stroke-genie-100" />
<span className="text-button text-genie-100 font-pretendard">
{isLoading ? '실행 중...' : '실행하기'}
</span>
</button>
) : (
<></>
)}
<div className="px-3 py-1.5 bg-gradient-genie-primary rounded-lg outline-1 outline-offset-[-1px] outline-white/20 flex justify-center items-center gap-2 cursor-pointer">
<Download className="size-3 stroke-genie-100" />
<div className="justify-start text-genie-100 text-button font-pretendard">내보내기</div>
</div>
</div>
</div>

{/* Tab Content */}
<div className="flex-1 overflow-auto">
{activeTab === 'editor' && <QueryEditor />}
{activeTab === 'results' && <QueryResults />}
{activeTab === 'editor' && <QueryEditor query={query} setQuery={setQuery} />}
{activeTab === 'results' && (
<QueryResults result={result} isLoading={isLoading} error={error} />
)}
</div>
</div>
)
Expand Down
126 changes: 88 additions & 38 deletions src/renderer/src/components/workspace/query-panel/query-results.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,93 @@
/**
* @author nahyeongjin1
* @summary 쿼리 실행 결과 패널
* @returns JSX.Element
*/
export default function QueryResults(): React.JSX.Element {
return (
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 p-5 overflow-auto">
<div className="self-stretch rounded-lg outline-1 outline-offset-[-1px] outline-neutral-700 flex flex-col justify-start items-start overflow-hidden">
<div className="self-stretch bg-neutral-700 inline-flex justify-start items-center">
<div className="flex-1 p-2 justify-start text-neutral-200 text-xs font-medium font-['Pretendard'] leading-none">
상품명
</div>
<div className="flex-1 p-2 justify-start text-neutral-200 text-xs font-medium font-['Pretendard'] leading-none">
판매량
</div>
<div className="flex-1 p-2 justify-start text-neutral-200 text-xs font-medium font-['Pretendard'] leading-none">
매출액
</div>
</div>
{/* Table Rows */}
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className="self-stretch border-b border-neutral-700 last:border-b-0 inline-flex justify-start items-center"
>
<div className="flex-1 p-2 justify-start text-neutral-200 text-xs font-medium font-['Pretendard'] leading-none">
item
</div>
<div className="flex-1 p-2 justify-start text-neutral-200 text-xs font-medium font-['Pretendard'] leading-none">
1000
</div>
<div className="flex-1 p-2 justify-start text-neutral-200 text-xs font-medium font-['Pretendard'] leading-none">
300,000
</div>
</div>
))}
export interface QueryResultData {
columns: string[]
rows: (string | number | null)[][]
}

interface QueryResultsProps {
result: QueryResultData | null
isLoading: boolean
error: string | null
}

export default function QueryResults({
result,
isLoading,
error
}: QueryResultsProps): React.JSX.Element {
const hasResults = result && result.columns.length > 0

const renderContent = (): React.ReactNode => {
if (isLoading) {
return (
<div className="flex items-center justify-center h-full text-neutral-400">
쿼리를 실행 중입니다...
</div>
)
}

if (error) {
return (
<div className="flex items-center justify-center h-full text-red-500">오류: {error}</div>
)
}

if (hasResults) {
return (
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-neutral-700">
<tr>
{result.columns.map((column) => (
<th
key={column}
scope="col"
className="px-4 py-2 text-left text-xs font-medium text-neutral-200 tracking-wider"
>
{column}
</th>
))}
</tr>
</thead>
<tbody>
{result.rows.length > 0 ? (
result.rows.map((row, rowIndex) => (
<tr key={rowIndex} className="border-b border-neutral-700">
{row.map((cell, cellIndex) => (
<td
key={cellIndex}
className="px-4 py-2 whitespace-nowrap text-sm text-neutral-200"
>
{String(cell)}
</td>
))}
</tr>
))
) : (
<tr>
<td
colSpan={result.columns.length}
className="text-center py-4 text-sm text-neutral-400"
>
쿼리는 성공했지만 반환된 행이 없습니다.
</td>
</tr>
)}
</tbody>
</table>
</div>
)
}

return (
<div className="flex items-center justify-center h-full text-neutral-500">
쿼리를 실행하여 결과를 확인하세요.
</div>
)
}

return (
<div className="flex-1 flex flex-col h-full">
<div className="flex-1 p-5 overflow-auto">{renderContent()}</div>
</div>
)
}