@@ -59,6 +59,8 @@ export class ProcessViewerViewModel implements ViewModel {
5959 containerHeightAtom : jotai . PrimitiveAtom < number > ;
6060 loadingAtom : jotai . PrimitiveAtom < boolean > ;
6161 errorAtom : jotai . PrimitiveAtom < string > ;
62+ lastSuccessAtom : jotai . PrimitiveAtom < number > ;
63+ pausedAtom : jotai . PrimitiveAtom < boolean > ;
6264
6365 connection : jotai . Atom < string > ;
6466 connStatus : jotai . Atom < ConnStatus > ;
@@ -78,6 +80,8 @@ export class ProcessViewerViewModel implements ViewModel {
7880 this . containerHeightAtom = jotai . atom < number > ( 0 ) ;
7981 this . loadingAtom = jotai . atom < boolean > ( true ) ;
8082 this . errorAtom = jotai . atom < string > ( null ) as jotai . PrimitiveAtom < string > ;
83+ this . lastSuccessAtom = jotai . atom < number > ( 0 ) as jotai . PrimitiveAtom < number > ;
84+ this . pausedAtom = jotai . atom < boolean > ( false ) as jotai . PrimitiveAtom < boolean > ;
8185
8286 this . connection = jotai . atom ( ( get ) => {
8387 const connValue = get ( this . env . getBlockMetaKeyAtom ( blockId , "connection" ) ) ;
@@ -135,6 +139,7 @@ export class ProcessViewerViewModel implements ViewModel {
135139 globalStore . set ( this . dataAtom , resp ) ;
136140 globalStore . set ( this . loadingAtom , false ) ;
137141 globalStore . set ( this . errorAtom , null ) ;
142+ globalStore . set ( this . lastSuccessAtom , Date . now ( ) ) ;
138143 }
139144 ( window as any ) . RPL = resp ; // debugging (remove before commit)
140145 } catch ( e ) {
@@ -166,7 +171,21 @@ export class ProcessViewerViewModel implements ViewModel {
166171 this . cancelPoll ( ) ;
167172 }
168173 this . cancelPoll = null ;
169- this . startPolling ( ) ;
174+ if ( ! globalStore . get ( this . pausedAtom ) ) {
175+ this . startPolling ( ) ;
176+ }
177+ }
178+
179+ setPaused ( paused : boolean ) {
180+ globalStore . set ( this . pausedAtom , paused ) ;
181+ if ( paused ) {
182+ if ( this . cancelPoll ) {
183+ this . cancelPoll ( ) ;
184+ }
185+ this . cancelPoll = null ;
186+ } else {
187+ this . startPolling ( ) ;
188+ }
170189 }
171190
172191 setSort ( col : SortCol ) {
@@ -244,6 +263,65 @@ const SortIndicator = React.memo(function SortIndicator({ active, desc }: { acti
244263} ) ;
245264SortIndicator . displayName = "SortIndicator" ;
246265
266+ const StatusIndicator = React . memo ( function StatusIndicator ( { model } : { model : ProcessViewerViewModel } ) {
267+ const paused = jotai . useAtomValue ( model . pausedAtom ) ;
268+ const error = jotai . useAtomValue ( model . errorAtom ) ;
269+ const lastSuccess = jotai . useAtomValue ( model . lastSuccessAtom ) ;
270+ const [ now , setNow ] = React . useState ( ( ) => Date . now ( ) ) ;
271+
272+ React . useEffect ( ( ) => {
273+ if ( paused ) return ;
274+ const id = setInterval ( ( ) => setNow ( Date . now ( ) ) , 1000 ) ;
275+ return ( ) => clearInterval ( id ) ;
276+ } , [ paused ] ) ;
277+
278+ if ( paused ) {
279+ const tooltipContent = (
280+ < div className = "flex flex-col gap-0.5" >
281+ < span > Paused</ span >
282+ < span className = "text-muted" > Click to resume</ span >
283+ </ div >
284+ ) ;
285+ return (
286+ < Tooltip content = { tooltipContent } placement = "bottom" >
287+ < div
288+ className = "flex items-center justify-center w-4 h-4 cursor-pointer text-warning hover:opacity-80 transition-opacity"
289+ onClick = { ( ) => model . setPaused ( false ) }
290+ >
291+ < svg viewBox = "0 0 16 16" fill = "currentColor" className = "w-3.5 h-3.5" >
292+ < rect x = "2" y = "2" width = "4" height = "12" rx = "1" />
293+ < rect x = "10" y = "2" width = "4" height = "12" rx = "1" />
294+ </ svg >
295+ </ div >
296+ </ Tooltip >
297+ ) ;
298+ }
299+
300+ const stalled = lastSuccess > 0 && now - lastSuccess > 5000 ;
301+ const circleColor = error != null ? "text-error" : stalled ? "text-warning" : "text-success" ;
302+ const statusLabel = error != null ? "Error" : stalled ? "Stalled" : "Updating" ;
303+ const tooltipContent = (
304+ < div className = "flex flex-col gap-0.5" >
305+ < span > { statusLabel } </ span >
306+ < span className = "text-muted" > Click to pause</ span >
307+ </ div >
308+ ) ;
309+
310+ return (
311+ < Tooltip content = { tooltipContent } placement = "bottom" >
312+ < div
313+ className = { `flex items-center justify-center w-4 h-4 cursor-pointer ${ circleColor } hover:opacity-80 transition-opacity` }
314+ onClick = { ( ) => model . setPaused ( true ) }
315+ >
316+ < svg viewBox = "0 0 16 16" fill = "currentColor" className = "w-3 h-3" >
317+ < circle cx = "8" cy = "8" r = "6" />
318+ </ svg >
319+ </ div >
320+ </ Tooltip >
321+ ) ;
322+ } ) ;
323+ StatusIndicator . displayName = "StatusIndicator" ;
324+
247325const TableHeader = React . memo ( function TableHeader ( {
248326 model,
249327 sortBy,
@@ -358,33 +436,40 @@ export const ProcessViewerView: React.FC<ViewComponentProps<ProcessViewerViewMod
358436 const summary = data ?. summary ;
359437 const memUsedGb = summary ?. memused != null ? ( summary . memused / 1024 / 1024 / 1024 ) . toFixed ( 1 ) : null ;
360438 const memTotalGb = summary ?. memtotal != null ? ( summary . memtotal / 1024 / 1024 / 1024 ) . toFixed ( 1 ) : null ;
361- const cpuPct = summary ?. cpusum != null && summary ?. numcpu != null && summary . numcpu > 0 ? ( summary . cpusum / summary . numcpu ) . toFixed ( 1 ) : null ;
439+ const cpuPct =
440+ summary ?. cpusum != null && summary ?. numcpu != null && summary . numcpu > 0
441+ ? ( summary . cpusum / summary . numcpu ) . toFixed ( 1 ) . padStart ( 5 , " " )
442+ : null ;
362443
363444 return (
364445 < div className = "flex flex-col w-full h-full overflow-hidden" ref = { containerRef } >
365446 { /* status bar */ }
366447 < div className = "flex shrink-0 items-center gap-4 px-2 py-1 text-xs text-secondary border-b border-white/10 bg-panel" >
448+ < StatusIndicator model = { model } />
367449 { summary != null && (
368450 < >
369451 { summary . load1 != null && (
370452 < span >
371- Load: { summary . load1 . toFixed ( 2 ) } { summary . load5 . toFixed ( 2 ) } { " " }
372- { summary . load15 . toFixed ( 2 ) }
453+ Load:{ " " }
454+ < span className = "font-mono" >
455+ { summary . load1 . toFixed ( 2 ) } { summary . load5 . toFixed ( 2 ) } { " " }
456+ { summary . load15 . toFixed ( 2 ) }
457+ </ span >
373458 </ span >
374459 ) }
375460 { memUsedGb != null && (
376461 < span >
377- Mem: { memUsedGb } G / { memTotalGb } G
462+ Mem: < span className = "font-mono" > { memUsedGb } G / { memTotalGb } G</ span >
378463 </ span >
379464 ) }
380465 { cpuPct != null && (
381466 < span >
382- CPU: { cpuPct } % ({ summary . numcpu } cores)
467+ CPU: < span className = "font-mono whitespace-pre" > { cpuPct } % ({ summary . numcpu } cores)</ span >
383468 </ span >
384469 ) }
385470 </ >
386471 ) }
387- < span className = "ml-auto" >
472+ < span className = "ml-auto font-mono " >
388473 { totalCount > 0
389474 ? filteredCount < totalCount
390475 ? `${ filteredCount } / ${ totalCount } processes`
0 commit comments