@@ -22,18 +22,19 @@ import { Box, Text, render } from "ink"
2222import type Group from "./Group.js"
2323import type {
2424 Context ,
25- ChangeContextRequest ,
2625 ChangeContextRequestHandler ,
2726 WatcherInitializer ,
27+ UpdateError ,
2828 UpdatePayload ,
29+ UpdatePayloadOrError ,
2930 ResourceSpec ,
3031} from "./types.js"
3132
33+ import Header from "./Header.js"
3234import JobBox from "./JobBox.js"
35+ import { isError } from "./types.js"
3336import defaultValueFor from "./defaults.js"
3437
35- import Header from "./Header.js"
36-
3738type UI = {
3839 /** Force a refresh */
3940 refreshCycle ?: number
@@ -55,6 +56,9 @@ type State = UI & {
5556 /** Model from controller */
5657 rawModel : UpdatePayload
5758
59+ /** Error in updating model? */
60+ updateError : null | UpdateError
61+
5862 /** Our grouping of `rawModel` */
5963 groups : Group [ ]
6064
@@ -83,19 +87,19 @@ class Top extends React.PureComponent<Props, State> {
8387 return this . state ?. selectedGroupIdx >= 0 && this . state ?. selectedGroupIdx < this . state . groups . length
8488 }
8589
90+ private clearCurrentJobSelection ( ) {
91+ this . setState ( { selectedGroupIdx : - 1 } )
92+ }
93+
8694 /** Current cluster context */
8795 private get currentContext ( ) {
8896 return {
97+ context : this . state ?. rawModel ?. context || this . props . context ,
8998 cluster : this . state ?. rawModel ?. cluster || this . props . cluster ,
9099 namespace : this . state ?. rawModel ?. namespace || this . props . namespace ,
91100 }
92101 }
93102
94- /** Updated cluster context */
95- private updatedContext ( { which } : Pick < ChangeContextRequest , "which" > , next : string ) {
96- return Object . assign ( this . currentContext , which === "namespace" ? { namespace : next } : { cluster : next } )
97- }
98-
99103 public async componentDidMount ( ) {
100104 this . setState ( { watcher : await this . props . initWatcher ( this . currentContext , this . onData ) } )
101105
@@ -131,6 +135,28 @@ class Top extends React.PureComponent<Props, State> {
131135 }
132136 }
133137
138+ private async cycleThroughContexts ( which : "namespace" | "cluster" , dir : "up" | "down" ) {
139+ if ( this . currentContext ) {
140+ const updatedContext = await this . props . changeContext ( { which, context : this . currentContext , dir } )
141+
142+ if ( updatedContext ) {
143+ this . reinit ( updatedContext )
144+ }
145+ }
146+ }
147+
148+ private cycleThroughJobs ( dir : "left" | "right" ) {
149+ if ( this . state . groups ) {
150+ const incr = dir === "left" ? - 1 : 1
151+ this . setState ( ( curState ) => ( {
152+ selectedGroupIdx :
153+ curState ?. selectedGroupIdx === undefined
154+ ? 0
155+ : this . mod ( curState . selectedGroupIdx + incr , curState . groups . length + 1 ) ,
156+ } ) )
157+ }
158+ }
159+
134160 /** Handle keyboard events from the user */
135161 private initKeyboardEvents ( ) {
136162 if ( ! process . stdin . isTTY ) {
@@ -149,46 +175,23 @@ class Top extends React.PureComponent<Props, State> {
149175 } else {
150176 switch ( key . name ) {
151177 case "escape" :
152- this . setState ( { selectedGroupIdx : - 1 } )
178+ this . clearCurrentJobSelection ( )
153179 break
180+
154181 case "up" :
155182 case "down" :
156- /** Change context selection */
157- if ( this . state ?. rawModel . namespace ) {
158- this . props
159- . changeContext ( { which : "namespace" , from : this . state . rawModel . namespace , dir : key . name } )
160- . then ( ( next ) => {
161- if ( next ) {
162- this . reinit ( this . updatedContext ( { which : "namespace" } , next ) )
163- }
164- } )
165- }
183+ this . cycleThroughContexts ( "namespace" , key . name )
184+ break
185+
186+ case "pageup" :
187+ case "pagedown" :
188+ this . cycleThroughContexts ( "cluster" , key . name === "pageup" ? "up" : "down" )
166189 break
167190
168191 case "left" :
169192 case "right" :
170- /** Change job selection */
171- if ( this . state . groups ) {
172- const incr = key . name === "left" ? - 1 : 1
173- this . setState ( ( curState ) => ( {
174- selectedGroupIdx :
175- curState ?. selectedGroupIdx === undefined
176- ? 0
177- : this . mod ( curState . selectedGroupIdx + incr , curState . groups . length + 1 ) ,
178- } ) )
179- }
193+ this . cycleThroughJobs ( key . name )
180194 break
181- /*case "i":
182- this.setState((curState) => ({ blockCells: !this.useBlocks(curState) }))
183- break*/
184- /*case "g":
185- this.setState((curState) => ({
186- groupHosts: !this.groupHosts(curState),
187- groups: !curState?.rawModel
188- ? curState?.groups
189- : this.groupBy(curState.rawModel, !this.groupHosts(curState)),
190- }))
191- break */
192195 }
193196 }
194197 } )
@@ -198,28 +201,38 @@ class Top extends React.PureComponent<Props, State> {
198201 return { min : { cpu : 0 , mem : 0 , gpu : 0 } , tot : { } }
199202 }
200203
201- private reinit ( context : Context ) {
204+ private async reinit ( context : Context ) {
202205 if ( this . state ?. watcher ) {
203206 this . state ?. watcher . kill ( )
204207 }
205- this . setState ( { groups : [ ] , rawModel : Object . assign ( { hosts : [ ] , stats : this . emptyStats } , context ) } )
206- this . props . initWatcher ( context , this . onData )
208+ this . setState ( {
209+ groups : [ ] ,
210+ updateError : null ,
211+ watcher : await this . props . initWatcher ( context , this . onData ) ,
212+ rawModel : Object . assign ( { hosts : [ ] , stats : this . emptyStats } , context ) ,
213+ } )
207214 }
208215
209216 /** We have received data from the controller */
210- private readonly onData = ( rawModel : UpdatePayload ) => {
217+ private readonly onData = ( rawModel : UpdatePayloadOrError ) => {
211218 if ( rawModel . cluster !== this . currentContext . cluster || rawModel . namespace !== this . currentContext . namespace ) {
212219 // this is straggler data from the prior context
213220 return
214- }
215-
216- this . setState ( ( curState ) => {
217- if ( JSON . stringify ( curState ?. rawModel ) === JSON . stringify ( rawModel ) ) {
218- return null
219- } else {
220- return { rawModel, groups : this . groupBy ( rawModel ) }
221+ } else if ( isError ( rawModel ) ) {
222+ // update error
223+ if ( ! this . state ?. updateError || JSON . stringify ( rawModel ) !== JSON . stringify ( this . state . updateError ) ) {
224+ this . setState ( { updateError : rawModel } )
221225 }
222- } )
226+ } else {
227+ // good update from current context
228+ this . setState ( ( curState ) => {
229+ if ( JSON . stringify ( curState ?. rawModel ) === JSON . stringify ( rawModel ) ) {
230+ return null
231+ } else {
232+ return { rawModel, groups : this . groupBy ( rawModel ) }
233+ }
234+ } )
235+ }
223236 }
224237
225238 private groupBy ( model : UpdatePayload ) : State [ "groups" ] {
@@ -272,7 +285,9 @@ class Top extends React.PureComponent<Props, State> {
272285 }
273286
274287 private body ( ) {
275- if ( this . state . groups . length === 0 ) {
288+ if ( this . state ?. updateError ) {
289+ return < Text color = "red" > { this . state . updateError . message } </ Text >
290+ } else if ( this . state . groups . length === 0 ) {
276291 return < Text > No active jobs</ Text >
277292 } else {
278293 return (
@@ -291,13 +306,13 @@ class Top extends React.PureComponent<Props, State> {
291306 }
292307
293308 public render ( ) {
294- if ( ! this . state ?. groups ) {
309+ if ( ! this . state ?. updateError && ! this . state ?. groups ) {
295310 // TODO spinner? this means we haven't received the first data set, yet
296311 return < React . Fragment />
297312 } else {
298313 return (
299314 < Box flexDirection = "column" >
300- < Header cluster = { this . state . rawModel . cluster } namespace = { this . state . rawModel . namespace } />
315+ < Header { ... this . currentContext } />
301316 < Box marginTop = { 1 } > { this . body ( ) } </ Box >
302317 </ Box >
303318 )
0 commit comments