@@ -35,6 +35,7 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
3535 const [ executions , setExecutions ] = useState < WorkloadExecution [ ] > ( [ ] ) ;
3636 const [ wlForm , setWlForm ] = useState ( { name : '' , description : '' , count : 100 } ) ;
3737 const [ wlLoading , setWlLoading ] = useState ( false ) ;
38+ const [ selectedWorkloadForQueries , setSelectedWorkloadForQueries ] = useState < Workload | null > ( null ) ;
3839
3940 // --- Monitor ---
4041 const [ selectedExecution , setSelectedExecution ] = useState < WorkloadExecution | null > ( null ) ;
@@ -188,6 +189,20 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
188189 }
189190 } ;
190191
192+ const handleViewQueries = async ( id : string ) => {
193+ try {
194+ const res = await api ( `/admin/workloads/${ id } ` ) ;
195+ if ( res . ok ) {
196+ const data = await res . json ( ) ;
197+ setSelectedWorkloadForQueries ( data ) ;
198+ } else {
199+ Swal . fire ( "실패" , "쿼리 정보를 불러오지 못했습니다." , "error" ) ;
200+ }
201+ } catch ( e ) {
202+ console . error ( e ) ;
203+ }
204+ } ;
205+
191206 // --- Charts ---
192207 const chartData : EquityPoint [ ] = useMemo ( ( ) => {
193208 return executions
@@ -205,6 +220,17 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
205220 } ) ) ;
206221 } , [ executions , workloads ] ) ;
207222
223+ // --- Components ---
224+ const StatCard = ( { label, value, unit, color } : { label : string , value : string | number , unit ?: string , color ?: string } ) => (
225+ < div style = { { background : '#f9fafb' , borderRadius : 12 , padding : '20px' , border : '1px solid #e5e7eb' , display : 'flex' , flexDirection : 'column' , alignItems : 'center' , justifyContent : 'center' , boxShadow : '0 1px 2px rgba(0,0,0,0.05)' } } >
226+ < span style = { { fontSize : 13 , color : '#6b7280' , textTransform : 'uppercase' , fontWeight : 600 , letterSpacing : '0.025em' , marginBottom : 8 } } > { label } </ span >
227+ < div style = { { display : 'flex' , alignItems : 'baseline' , gap : 4 } } >
228+ < span style = { { fontSize : 28 , fontWeight : 700 , color : color || '#111827' , fontFamily : 'var(--font-mono)' } } > { value } </ span >
229+ { unit && < span style = { { fontSize : 14 , color : '#9ca3af' , fontWeight : 500 } } > { unit } </ span > }
230+ </ div >
231+ </ div >
232+ ) ;
233+
208234 return (
209235 < div className = "app-shell" >
210236 < header className = "top-header" style = { { background : '#1a1a1a' , borderBottom : '1px solid #333' } } >
@@ -243,7 +269,6 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
243269 </ header >
244270
245271 < main className = "main-content" >
246-
247272 { /* 1. Dashboard Stats */ }
248273 { activeTab === 'stats' && stats && (
249274 < >
@@ -374,7 +399,12 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
374399 < tr key = { w . id } >
375400 < td > { w . name } </ td >
376401 < td > { w . query_count } </ td >
377- < td > < button className = "btn btn--secondary btn--sm" onClick = { ( ) => handleExecuteWorkload ( w . id ) } > ▶ 실행</ button > </ td >
402+ < td >
403+ < div style = { { display :'flex' , gap : 4 } } >
404+ < button className = "btn btn--secondary btn--sm" onClick = { ( ) => handleViewQueries ( w . id ) } > 조회</ button >
405+ < button className = "btn btn--secondary btn--sm" onClick = { ( ) => handleExecuteWorkload ( w . id ) } > ▶ 실행</ button >
406+ </ div >
407+ </ td >
378408 </ tr >
379409 ) ) }
380410 </ tbody >
@@ -422,28 +452,116 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
422452 ) }
423453 </ main >
424454
425- { /* Analysis Modal */ }
455+ { /* Queries Modal */ }
456+ { selectedWorkloadForQueries && (
457+ < Modal title = { `워크로드 쿼리 목록 (${ selectedWorkloadForQueries . name } )` } onClose = { ( ) => setSelectedWorkloadForQueries ( null ) } width = { 900 } >
458+ < div style = { { maxHeight : '60vh' , overflowY : 'auto' } } >
459+ { selectedWorkloadForQueries . queries && selectedWorkloadForQueries . queries . length > 0 ? (
460+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : 16 } } >
461+ { selectedWorkloadForQueries . queries . map ( ( q , idx ) => (
462+ < div key = { idx } style = { { background : '#f5f5f5' , padding : 16 , borderRadius : 8 , border : '1px solid #e5e5e5' } } >
463+ < div style = { { fontSize : 12 , fontWeight : 700 , color : '#666' , marginBottom : 8 , display : 'flex' , justifyContent : 'space-between' } } >
464+ < span > Query #{ idx + 1 } </ span >
465+ </ div >
466+ < pre style = { { margin : 0 , whiteSpace : 'pre-wrap' , fontFamily : 'var(--font-mono)' , fontSize : 13 , color : '#2563eb' , background : '#fff' , padding : 12 , borderRadius : 4 , border : '1px solid #eaeaea' } } >
467+ { q . sql }
468+ </ pre >
469+ < div style = { { marginTop : 12 } } >
470+ < span style = { { fontSize : 11 , fontWeight : 600 , color : '#888' , textTransform : 'uppercase' } } > Parameters</ span >
471+ < div style = { { marginTop : 4 , fontSize : 12 , fontFamily : 'var(--font-mono)' , color : '#444' } } >
472+ { JSON . stringify ( q . params ) }
473+ </ div >
474+ </ div >
475+ </ div >
476+ ) ) }
477+ </ div >
478+ ) : (
479+ < div className = "empty-state-small" > 생성된 쿼리 정보가 없습니다.</ div >
480+ ) }
481+ </ div >
482+ </ Modal >
483+ ) }
484+
485+ { /* Analysis Modal (Improved UI) */ }
426486 { selectedExecution && (
427- < Modal title = "성능 상세 분석" onClose = { ( ) => setSelectedExecution ( null ) } >
428- < div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 24 } } >
487+ < Modal title = "성능 상세 분석" onClose = { ( ) => setSelectedExecution ( null ) } width = { 1000 } >
488+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : 32 } } >
489+ { /* Header Section */ }
490+ < div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , paddingBottom : 24 , borderBottom : '1px solid #eaeaea' } } >
491+ < div >
492+ < div style = { { fontSize : 14 , color : '#6b7280' , marginBottom : 4 } } > Workload Execution ID</ div >
493+ < div style = { { fontFamily : 'var(--font-mono)' , fontSize : 14 , fontWeight : 600 } } > { selectedExecution . id } </ div >
494+ </ div >
495+ < div style = { { textAlign : 'right' } } >
496+ < div style = { { fontSize : 14 , color : '#6b7280' , marginBottom : 4 } } > Executed At</ div >
497+ < div style = { { fontWeight : 600 } } > { new Date ( selectedExecution . created_at ) . toLocaleString ( ) } </ div >
498+ </ div >
499+ </ div >
500+
501+ { /* Metrics Grid */ }
429502 < div >
430- < h4 style = { { marginBottom : 12 } } > 📊 상세 성능 지표</ h4 >
431- { selectedExecution . extended_metrics ? (
432- < div className = "metric-grid-compact" style = { { gridTemplateColumns : '1fr 1fr' } } >
433- < div className = "metric-box" > < label > Buffer Hit Ratio</ label > < span > { selectedExecution . extended_metrics . buffer_hit_ratio } %</ span > </ div >
434- < div className = "metric-box" > < label > Disk Blocks Read</ label > < span > { selectedExecution . extended_metrics . blocks_read } </ span > </ div >
435- < div className = "metric-box" > < label > Buffer Blocks Hit</ label > < span > { selectedExecution . extended_metrics . blocks_hit } </ span > </ div >
436- < div className = "metric-box" > < label > Rows Returned</ label > < span > { selectedExecution . extended_metrics . tuples_returned } </ span > </ div >
437- < div className = "metric-box" > < label > Transactions</ label > < span > { selectedExecution . extended_metrics . transactions } </ span > </ div >
438- </ div >
439- ) : (
440- < div className = "empty-state-small" > 추가 지표 없음</ div >
441- ) }
503+ < h4 style = { { fontSize : 16 , fontWeight : 700 , marginBottom : 16 , color : '#111827' } } > 📊 Performance Metrics</ h4 >
504+ < div style = { { display : 'grid' , gridTemplateColumns : 'repeat(4, 1fr)' , gap : 16 } } >
505+ < StatCard
506+ label = "Execution Time"
507+ value = { selectedExecution . execution_time_ms . toFixed ( 2 ) }
508+ unit = "ms"
509+ color = "#2563eb"
510+ />
511+ { selectedExecution . extended_metrics ? (
512+ < >
513+ < StatCard
514+ label = "Buffer Hit Ratio"
515+ value = { selectedExecution . extended_metrics . buffer_hit_ratio }
516+ unit = "%"
517+ color = { selectedExecution . extended_metrics . buffer_hit_ratio > 90 ? '#059669' : '#d97706' }
518+ />
519+ < StatCard
520+ label = "Disk Blocks Read"
521+ value = { selectedExecution . extended_metrics . blocks_read }
522+ />
523+ < StatCard
524+ label = "Buffer Blocks Hit"
525+ value = { selectedExecution . extended_metrics . blocks_hit }
526+ />
527+ < StatCard
528+ label = "Rows Returned"
529+ value = { selectedExecution . extended_metrics . tuples_returned }
530+ />
531+ < StatCard
532+ label = "Rows Fetched"
533+ value = { selectedExecution . extended_metrics . tuples_fetched }
534+ />
535+ < StatCard
536+ label = "Transactions"
537+ value = { selectedExecution . extended_metrics . transactions }
538+ />
539+ </ >
540+ ) : (
541+ < div style = { { gridColumn : 'span 3' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , color : '#9ca3af' , background : '#f9fafb' , borderRadius : 12 } } >
542+ No extended metrics available
543+ </ div >
544+ ) }
545+ </ div >
442546 </ div >
547+
548+ { /* Config Snapshot */ }
443549 < div >
444- < h4 style = { { marginBottom : 12 } } > ⚙️ DB 파라미터 스냅샷</ h4 >
445- < div style = { { maxHeight : '300px' , overflowY : 'auto' , background : '#f5f5f5' , padding : 12 , borderRadius : 6 , fontSize : 12 } } >
446- < pre style = { { margin : 0 } } > { JSON . stringify ( selectedExecution . db_config_snapshot , null , 2 ) } </ pre >
550+ < h4 style = { { fontSize : 16 , fontWeight : 700 , marginBottom : 16 , color : '#111827' } } > ⚙️ DB Parameter Snapshot</ h4 >
551+ < div style = { {
552+ background : '#1e293b' ,
553+ color : '#e2e8f0' ,
554+ padding : '20px' ,
555+ borderRadius : 12 ,
556+ fontFamily : 'var(--font-mono)' ,
557+ fontSize : 13 ,
558+ maxHeight : '300px' ,
559+ overflowY : 'auto' ,
560+ boxShadow : 'inset 0 2px 4px rgba(0,0,0,0.1)'
561+ } } >
562+ < pre style = { { margin : 0 } } >
563+ { JSON . stringify ( selectedExecution . db_config_snapshot , null , 2 ) }
564+ </ pre >
447565 </ div >
448566 </ div >
449567 </ div >
0 commit comments