@@ -17,6 +17,8 @@ interface QueryResult {
1717 alignments : Alignment [ ]
1818}
1919
20+ const ITEMS_PER_PAGE = 25
21+
2022const SAMPLE_QUERIES = [
2123 {
2224 label : 'Basic' ,
@@ -58,6 +60,30 @@ export function QueryView() {
5860 const [ loading , setLoading ] = useState ( false )
5961 const [ error , setError ] = useState < string | null > ( null )
6062 const [ result , setResult ] = useState < QueryResult | null > ( null )
63+ const [ totalCount , setTotalCount ] = useState ( 0 )
64+ const [ currentPage , setCurrentPage ] = useState ( 1 )
65+ const [ executedQuery , setExecutedQuery ] = useState ( '' )
66+
67+ const fetchPage = async ( queryStr : string , page : number ) => {
68+ const response = await fetch ( '/api/query/fetch_override' , {
69+ method : 'POST' ,
70+ headers : {
71+ 'Content-Type' : 'application/json' ,
72+ } ,
73+ body : JSON . stringify ( {
74+ query : queryStr ,
75+ limit_override : ITEMS_PER_PAGE ,
76+ offset_add : ( page - 1 ) * ITEMS_PER_PAGE ,
77+ } ) ,
78+ } )
79+
80+ if ( ! response . ok ) {
81+ const errorText = await response . text ( )
82+ throw new Error ( errorText || 'Query execution failed' )
83+ }
84+
85+ return response . json ( )
86+ }
6187
6288 const handleExecuteQuery = async ( ) => {
6389 if ( ! query . trim ( ) ) {
@@ -69,24 +95,49 @@ export function QueryView() {
6995 setError ( null )
7096
7197 try {
72- const response = await fetch ( '/api/query/execute' , {
98+ // Get count first
99+ const countResponse = await fetch ( '/api/query/count_raw' , {
73100 method : 'POST' ,
74101 headers : {
75102 'Content-Type' : 'application/json' ,
76103 } ,
77104 body : JSON . stringify ( { query : query . trim ( ) } ) ,
78105 } )
79106
80- if ( ! response . ok ) {
81- const errorText = await response . text ( )
82- throw new Error ( errorText || 'Query execution failed' )
107+ if ( ! countResponse . ok ) {
108+ const errorText = await countResponse . text ( )
109+ throw new Error ( errorText || 'Count query failed' )
83110 }
84111
85- const data = await response . json ( )
112+ const countData = await countResponse . json ( )
113+ setTotalCount ( countData . count )
114+ setExecutedQuery ( query . trim ( ) )
115+ setCurrentPage ( 1 )
116+
117+ // Fetch first page
118+ const data = await fetchPage ( query . trim ( ) , 1 )
86119 setResult ( data )
87120 } catch ( err ) {
88121 setError ( err instanceof Error ? err . message : 'Failed to execute query' )
89122 setResult ( null )
123+ setTotalCount ( 0 )
124+ } finally {
125+ setLoading ( false )
126+ }
127+ }
128+
129+ const handlePageChange = async ( newPage : number ) => {
130+ if ( ! executedQuery ) return
131+
132+ setLoading ( true )
133+ setError ( null )
134+
135+ try {
136+ const data = await fetchPage ( executedQuery , newPage )
137+ setResult ( data )
138+ setCurrentPage ( newPage )
139+ } catch ( err ) {
140+ setError ( err instanceof Error ? err . message : 'Failed to fetch page' )
90141 } finally {
91142 setLoading ( false )
92143 }
@@ -96,6 +147,8 @@ export function QueryView() {
96147 setQuery ( sampleQuery )
97148 setError ( null )
98149 setResult ( null )
150+ setTotalCount ( 0 )
151+ setCurrentPage ( 1 )
99152 }
100153
101154 return (
@@ -178,35 +231,69 @@ export function QueryView() {
178231 { result && (
179232 < Card >
180233 < CardContent className = "p-0" >
181- < div className = "overflow-auto" >
182- < table className = "w-full border-collapse" >
183- < thead className = "bg-muted sticky top-0" >
184- < tr >
185- { result . columns . map ( ( col , index ) => (
186- < th
187- key = { index }
188- className = "border border-border px-4 py-2 font-medium text-center uppercase text-xs tracking-wide"
189- >
190- { col }
191- </ th >
192- ) ) }
193- </ tr >
194- </ thead >
195- < tbody >
196- { result . rows . map ( ( row , rowIndex ) => (
197- < tr key = { rowIndex } className = "hover:bg-muted/50" >
198- { row . map ( ( cell , cellIndex ) => (
199- < td
200- key = { cellIndex }
201- className = { `border border-border px-4 py-2 ${ getAlignmentClass ( result . alignments [ cellIndex ] ) } ` }
202- >
203- { cell }
204- </ td >
234+ < div className = "flex flex-col h-full" >
235+ { result . rows . length > 0 ? (
236+ < div className = "overflow-auto" >
237+ < table className = "w-full border-collapse" >
238+ < thead className = "bg-muted sticky top-0" >
239+ < tr >
240+ { result . columns . map ( ( col , index ) => (
241+ < th
242+ key = { index }
243+ className = "border border-border px-4 py-2 font-medium text-center uppercase text-xs tracking-wide"
244+ >
245+ { col }
246+ </ th >
247+ ) ) }
248+ </ tr >
249+ </ thead >
250+ < tbody >
251+ { result . rows . map ( ( row , rowIndex ) => (
252+ < tr key = { rowIndex } className = "hover:bg-muted/50" >
253+ { row . map ( ( cell , cellIndex ) => (
254+ < td
255+ key = { cellIndex }
256+ className = { `border border-border px-4 py-2 ${ getAlignmentClass ( result . alignments [ cellIndex ] ) } ` }
257+ >
258+ { cell }
259+ </ td >
260+ ) ) }
261+ </ tr >
205262 ) ) }
206- </ tr >
207- ) ) }
208- </ tbody >
209- </ table >
263+ </ tbody >
264+ </ table >
265+ </ div >
266+ ) : (
267+ < div className = "p-8 text-center text-muted-foreground" >
268+ No results found
269+ </ div >
270+ ) }
271+
272+ { /* Pagination */ }
273+ { totalCount > 0 && (
274+ < div className = "flex items-center justify-between p-4 border-t border-border" >
275+ < div className = "text-sm text-muted-foreground" >
276+ Showing { ( ( currentPage - 1 ) * ITEMS_PER_PAGE + 1 ) . toLocaleString ( ) } to{ ' ' }
277+ { Math . min ( currentPage * ITEMS_PER_PAGE , totalCount ) . toLocaleString ( ) } of { totalCount . toLocaleString ( ) }
278+ </ div >
279+ < div className = "flex items-center gap-2" >
280+ < button
281+ onClick = { ( ) => currentPage > 1 && handlePageChange ( currentPage - 1 ) }
282+ disabled = { currentPage === 1 || loading }
283+ className = "px-3 py-1.5 border border-border rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent hover:text-accent-foreground transition-colors"
284+ >
285+ Previous
286+ </ button >
287+ < button
288+ onClick = { ( ) => currentPage * ITEMS_PER_PAGE < totalCount && handlePageChange ( currentPage + 1 ) }
289+ disabled = { currentPage * ITEMS_PER_PAGE >= totalCount || loading }
290+ className = "px-3 py-1.5 border border-border rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent hover:text-accent-foreground transition-colors"
291+ >
292+ Next
293+ </ button >
294+ </ div >
295+ </ div >
296+ ) }
210297 </ div >
211298 </ CardContent >
212299 </ Card >
0 commit comments