11// LogsPage.jsx
22import { useState , useEffect , useRef } from 'react' ;
3- import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline" ;
3+ import { ClipboardDocumentListIcon , CubeIcon } from "@heroicons/react/24/outline" ;
44import { useParams } from 'react-router-dom' ;
55import api from '../../api/api' ;
66
77export default function LogsPage ( ) {
88 const { deploymentId } = useParams ( ) ;
99 const [ logs , setLogs ] = useState < any [ ] > ( [ ] ) ;
10+ const [ pods , setPods ] = useState ( [ ] ) ;
11+ const [ selectedPod , setSelectedPod ] = useState ( 'all' ) ; // 'all' or specific pod name
1012 const [ isConnected , setIsConnected ] = useState ( false ) ;
1113 const [ isConnecting , setIsConnecting ] = useState ( false ) ;
1214 const [ isLoadingHistory , setIsLoadingHistory ] = useState ( false ) ;
15+ const [ isLoadingPods , setIsLoadingPods ] = useState ( false ) ;
1316 const [ error , setError ] = useState < any > ( null ) ;
14- const wsRef : any = useRef ( null ) ;
17+ const wsRef = useRef < any > ( null ) ;
1518 const logsEndRef : any = useRef ( null ) ;
16- const reconnectTimeoutRef : any = useRef ( null ) ;
19+ const reconnectTimeoutRef = useRef < any > ( null ) ;
1720 const reconnectDelay = useRef ( 1000 ) ;
1821
1922 // Auto-scroll to bottom when new logs arrive
@@ -36,6 +39,29 @@ export default function LogsPage() {
3639 return null ;
3740 } ;
3841
42+ // Get WebSocket URL with proper protocol
43+ const getWebSocketUrl = ( ) => {
44+ const protocol = window . location . protocol === 'https:' ? 'wss:' : 'ws:' ;
45+ const host = window . location . host ;
46+ return `${ protocol } //${ host } ` ;
47+ } ;
48+
49+ // Load pods list
50+ const loadPods = async ( ) => {
51+ if ( ! deploymentId ) return ;
52+
53+ setIsLoadingPods ( true ) ;
54+ try {
55+ const response = await api . get ( `/v1/deployments/${ deploymentId } /pods` ) ;
56+ setPods ( response . data . pods || [ ] ) ;
57+ } catch ( err ) {
58+ console . error ( 'Failed to load pods:' , err ) ;
59+ // Don't set error for pods loading failure, just log it
60+ } finally {
61+ setIsLoadingPods ( false ) ;
62+ }
63+ } ;
64+
3965 // Load historical logs from API
4066 const loadHistoricalLogs = async ( retryCount = 0 ) => {
4167 if ( ! deploymentId ) return ;
@@ -44,7 +70,14 @@ export default function LogsPage() {
4470 setError ( null ) ;
4571
4672 try {
47- const response = await api . get ( `/v1/deployments/${ deploymentId } /logs?tail=100` ) ;
73+ let endpoint ;
74+ if ( selectedPod === 'all' ) {
75+ endpoint = `/v1/deployments/${ deploymentId } /logs?tail=100` ;
76+ } else {
77+ endpoint = `/v1/deployments/${ deploymentId } /pods/${ selectedPod } /logs?tail=100` ;
78+ }
79+
80+ const response = await api . get ( endpoint ) ;
4881
4982 if ( response . data . logs ) {
5083 // Parse historical logs - assuming they come as a single string with newlines
@@ -62,7 +95,8 @@ export default function LogsPage() {
6295 timestamp,
6396 message : line ,
6497 id : `history-${ index } ` ,
65- isHistorical : true
98+ isHistorical : true ,
99+ podName : selectedPod === 'all' ? 'merged' : selectedPod
66100 } ;
67101 } ) ;
68102
@@ -113,8 +147,18 @@ export default function LogsPage() {
113147 setIsConnecting ( true ) ;
114148 setError ( null ) ;
115149
116- // Add token to WebSocket URL as query parameter
117- const wsUrl = `ws://${ window . location . host } /v1/deployments/${ deploymentId } /logs/stream?tail=50&token=${ encodeURIComponent ( 'Bearer ' + token ) } ` ;
150+ // Build WebSocket URL based on selected pod with proper protocol
151+ const baseWsUrl = getWebSocketUrl ( ) ;
152+ let wsUrl ;
153+
154+ if ( selectedPod === 'all' ) {
155+ wsUrl = `${ baseWsUrl } /v1/deployments/${ deploymentId } /logs/stream?tail=50&token=${ encodeURIComponent ( 'Bearer ' + token ) } ` ;
156+ } else {
157+ wsUrl = `${ baseWsUrl } /v1/deployments/${ deploymentId } /pods/${ selectedPod } /logs/ws?tail=50&token=${ encodeURIComponent ( 'Bearer ' + token ) } ` ;
158+ }
159+
160+ console . log ( 'Connecting to WebSocket:' , wsUrl . replace ( / t o k e n = [ ^ & ] + / , 'token=***' ) ) ; // Log URL without token
161+
118162 const ws = new WebSocket ( wsUrl ) ;
119163
120164 ws . onopen = ( ) => {
@@ -125,10 +169,11 @@ export default function LogsPage() {
125169 } ;
126170
127171 ws . onmessage = ( event ) => {
128- console . log ( 'WebSocket message received:' , event ) ;
129172 // Skip connection confirmation messages
130173 if ( event . data === 'Connected to log stream' ||
174+ event . data . includes ( 'Connected to log stream for pod:' ) ||
131175 event . data === 'Log stream ended' ||
176+ event . data . includes ( 'Log stream ended for pod:' ) ||
132177 event . data . includes ( 'Authentication' ) ) {
133178 return ;
134179 }
@@ -138,10 +183,11 @@ export default function LogsPage() {
138183 timestamp,
139184 message : event . data ,
140185 id : `live-${ timestamp } -${ Math . random ( ) } ` ,
141- isHistorical : false
186+ isHistorical : false ,
187+ podName : selectedPod === 'all' ? 'merged' : selectedPod
142188 } ;
143189
144- setLogs ( ( prev : any ) => [ ...prev , newLog ] ) ;
190+ setLogs ( prev => [ ...prev , newLog ] ) ;
145191 } ;
146192
147193 ws . onerror = ( error ) => {
@@ -150,7 +196,7 @@ export default function LogsPage() {
150196 } ;
151197
152198 ws . onclose = ( event ) => {
153- console . log ( ' WebSocket disconnected' , event . code , event . reason ) ;
199+ console . log ( ` WebSocket disconnected: ${ event . code } ${ event . reason || '' } ` ) ;
154200 setIsConnected ( false ) ;
155201 setIsConnecting ( false ) ;
156202 wsRef . current = null ;
@@ -171,6 +217,7 @@ export default function LogsPage() {
171217 const delay = reconnectDelay . current ;
172218 reconnectDelay . current = Math . min ( delay * 2 , 30000 ) ; // Max 30s
173219
220+ console . log ( `Attempting to reconnect in ${ delay } ms (attempt ${ Math . log2 ( delay / 1000 ) + 1 } )` ) ;
174221 setError ( `Disconnected. Reconnecting in ${ delay / 1000 } s...` ) ;
175222
176223 reconnectTimeoutRef . current = setTimeout ( ( ) => {
@@ -181,9 +228,25 @@ export default function LogsPage() {
181228 wsRef . current = ws ;
182229 } ;
183230
184- // Initialize: Load history first, then connect WebSocket
231+ // Handle pod selection change
232+ const handlePodChange = ( podName : any ) => {
233+ if ( podName === selectedPod ) return ;
234+
235+ // Disconnect current WebSocket
236+ if ( wsRef . current ) {
237+ wsRef . current . close ( ) ;
238+ }
239+
240+ setSelectedPod ( podName ) ;
241+ setLogs ( [ ] ) ; // Clear current logs
242+ } ;
243+
244+ // Initialize: Load pods and history first, then connect WebSocket
185245 useEffect ( ( ) => {
186246 if ( deploymentId ) {
247+ // Load pods first
248+ loadPods ( ) ;
249+
187250 // Load historical logs first
188251 loadHistoricalLogs ( ) . then ( ( ) => {
189252 // Small delay to show historical logs before connecting WebSocket
@@ -204,15 +267,26 @@ export default function LogsPage() {
204267 } ;
205268 } , [ deploymentId ] ) ;
206269
270+ // Re-load logs when pod selection changes
271+ useEffect ( ( ) => {
272+ if ( deploymentId && selectedPod ) {
273+ loadHistoricalLogs ( ) . then ( ( ) => {
274+ setTimeout ( ( ) => {
275+ connectWebSocket ( ) ;
276+ } , 500 ) ;
277+ } ) ;
278+ }
279+ } , [ selectedPod ] ) ;
280+
207281 // Manual refresh - reload everything
208282 const handleRefresh = async ( ) => {
209283 setLogs ( [ ] ) ; // Clear logs
210284 if ( wsRef . current ) {
211285 wsRef . current . close ( ) ;
212286 }
213287
214- // Reload historical logs then reconnect WebSocket
215- await loadHistoricalLogs ( ) ;
288+ // Reload pods and historical logs then reconnect WebSocket
289+ await Promise . all ( [ loadPods ( ) , loadHistoricalLogs ( ) ] ) ;
216290 setTimeout ( ( ) => {
217291 connectWebSocket ( ) ;
218292 } , 500 ) ;
@@ -225,15 +299,16 @@ export default function LogsPage() {
225299
226300 // Download logs
227301 const handleDownload = ( ) => {
228- const logText = logs . map ( ( log ) =>
302+ const logText = logs . map ( log =>
229303 `[${ new Date ( log . timestamp ) . toLocaleString ( ) } ] ${ log . message } `
230304 ) . join ( '\n' ) ;
231305
232306 const blob = new Blob ( [ logText ] , { type : 'text/plain' } ) ;
233307 const url = URL . createObjectURL ( blob ) ;
234308 const a = document . createElement ( 'a' ) ;
235309 a . href = url ;
236- a . download = `logs-${ deploymentId } -${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .txt` ;
310+ const podSuffix = selectedPod === 'all' ? 'all-pods' : selectedPod ;
311+ a . download = `logs-${ deploymentId } -${ podSuffix } -${ new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] } .txt` ;
237312 a . click ( ) ;
238313 URL . revokeObjectURL ( url ) ;
239314 } ;
@@ -267,7 +342,7 @@ export default function LogsPage() {
267342 return (
268343 < div className = "bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden" >
269344 < div className = "px-8 py-6 bg-gray-50 border-b border-gray-200" >
270- < div className = "flex items-center justify-between" >
345+ < div className = "flex items-center justify-between mb-4 " >
271346 < div className = "flex items-center" >
272347 < div className = "w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center mr-4" >
273348 < ClipboardDocumentListIcon className = "h-6 w-6 text-gray-600" />
@@ -308,6 +383,44 @@ export default function LogsPage() {
308383 </ div >
309384 </ div >
310385
386+ { /* Pod Selection */ }
387+ < div className = "flex items-center space-x-4" >
388+ < div className = "flex items-center space-x-2" >
389+ < CubeIcon className = "h-5 w-5 text-gray-500" />
390+ < span className = "text-sm font-medium text-gray-700" > View logs from:</ span >
391+ </ div >
392+ < div className = "flex flex-wrap gap-2" >
393+ < button
394+ onClick = { ( ) => handlePodChange ( 'all' ) }
395+ disabled = { isLoadingPods }
396+ className = { `px-3 py-1 rounded-lg text-sm font-medium transition-all ${ selectedPod === 'all'
397+ ? 'bg-blue-600 text-white'
398+ : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
399+ } disabled:opacity-50 disabled:cursor-not-allowed`}
400+ >
401+ All Pods (Merged)
402+ </ button >
403+ { pods . map ( ( pod : any ) => (
404+ < button
405+ key = { pod . name }
406+ onClick = { ( ) => handlePodChange ( pod . name ) }
407+ disabled = { isLoadingPods }
408+ className = { `px-3 py-1 rounded-lg text-sm font-medium transition-all flex items-center space-x-1 ${ selectedPod === pod . name
409+ ? 'bg-blue-600 text-white'
410+ : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
411+ } disabled:opacity-50 disabled:cursor-not-allowed`}
412+ >
413+ < span > { pod . name } </ span >
414+ < span className = { `w-2 h-2 rounded-full ${ pod . ready ? 'bg-green-400' : 'bg-red-400'
415+ } `} > </ span >
416+ </ button >
417+ ) ) }
418+ { isLoadingPods && (
419+ < div className = "px-3 py-1 text-sm text-gray-500" > Loading pods...</ div >
420+ ) }
421+ </ div >
422+ </ div >
423+
311424 { error && (
312425 < div className = "mt-3 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-lg" >
313426 { error }
@@ -319,14 +432,19 @@ export default function LogsPage() {
319432 < div className = "bg-gray-900 text-white font-mono text-sm p-6 h-96 overflow-y-auto custom-scrollbar" >
320433 { logs . length > 0 ? (
321434 < >
322- { logs . map ( ( log ) => (
435+ { logs . map ( log => (
323436 < div key = { log . id } className = "flex space-x-4 py-1 hover:bg-gray-800 px-2 rounded group" >
324437 < span className = "text-gray-500 flex-shrink-0 select-none" >
325438 { new Date ( log . timestamp ) . toLocaleTimeString ( ) }
326439 </ span >
327440 < span className = { `flex-shrink-0 select-none ${ log . isHistorical ? 'text-gray-600' : 'text-green-400' } ` } >
328441 { log . isHistorical ? '◦' : '│' }
329442 </ span >
443+ { selectedPod === 'all' && log . podName !== 'merged' && (
444+ < span className = "text-blue-400 flex-shrink-0 select-none text-xs" >
445+ [{ log . podName } ]
446+ </ span >
447+ ) }
330448 < span className = "flex-1 break-all whitespace-pre-wrap" > { log . message } </ span >
331449 </ div >
332450 ) ) }
@@ -339,7 +457,10 @@ export default function LogsPage() {
339457 { isLoadingHistory || isConnecting ? 'Loading logs...' : 'No logs available at the moment.' }
340458 </ p >
341459 < p className = "text-gray-600 text-sm mt-2" >
342- Logs will appear here once your application starts generating them.
460+ { selectedPod === 'all'
461+ ? 'Logs from all pods will appear here once your application starts generating them.'
462+ : `Logs from pod "${ selectedPod } " will appear here once it starts generating them.`
463+ }
343464 </ p >
344465 </ div >
345466 ) }
@@ -363,6 +484,7 @@ export default function LogsPage() {
363484 < div className = "px-6 py-2 bg-gray-50 border-t border-gray-200 text-xs text-gray-500 flex justify-between" >
364485 < span >
365486 { logs . filter ( log => log . isHistorical ) . length } historical + { logs . filter ( log => ! log . isHistorical ) . length } live logs
487+ { selectedPod !== 'all' && ` from ${ selectedPod } ` }
366488 </ span >
367489 < span > Total: { logs . length } lines</ span >
368490 </ div >
@@ -386,4 +508,4 @@ export default function LogsPage() {
386508 ` } </ style >
387509 </ div >
388510 ) ;
389- }
511+ }
0 commit comments