@@ -22,18 +22,19 @@ import { Box, Text, render } from "ink"
22
22
import type Group from "./Group.js"
23
23
import type {
24
24
Context ,
25
- ChangeContextRequest ,
26
25
ChangeContextRequestHandler ,
27
26
WatcherInitializer ,
27
+ UpdateError ,
28
28
UpdatePayload ,
29
+ UpdatePayloadOrError ,
29
30
ResourceSpec ,
30
31
} from "./types.js"
31
32
33
+ import Header from "./Header.js"
32
34
import JobBox from "./JobBox.js"
35
+ import { isError } from "./types.js"
33
36
import defaultValueFor from "./defaults.js"
34
37
35
- import Header from "./Header.js"
36
-
37
38
type UI = {
38
39
/** Force a refresh */
39
40
refreshCycle ?: number
@@ -55,6 +56,9 @@ type State = UI & {
55
56
/** Model from controller */
56
57
rawModel : UpdatePayload
57
58
59
+ /** Error in updating model? */
60
+ updateError : null | UpdateError
61
+
58
62
/** Our grouping of `rawModel` */
59
63
groups : Group [ ]
60
64
@@ -83,19 +87,19 @@ class Top extends React.PureComponent<Props, State> {
83
87
return this . state ?. selectedGroupIdx >= 0 && this . state ?. selectedGroupIdx < this . state . groups . length
84
88
}
85
89
90
+ private clearCurrentJobSelection ( ) {
91
+ this . setState ( { selectedGroupIdx : - 1 } )
92
+ }
93
+
86
94
/** Current cluster context */
87
95
private get currentContext ( ) {
88
96
return {
97
+ context : this . state ?. rawModel ?. context || this . props . context ,
89
98
cluster : this . state ?. rawModel ?. cluster || this . props . cluster ,
90
99
namespace : this . state ?. rawModel ?. namespace || this . props . namespace ,
91
100
}
92
101
}
93
102
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
-
99
103
public async componentDidMount ( ) {
100
104
this . setState ( { watcher : await this . props . initWatcher ( this . currentContext , this . onData ) } )
101
105
@@ -131,6 +135,28 @@ class Top extends React.PureComponent<Props, State> {
131
135
}
132
136
}
133
137
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
+
134
160
/** Handle keyboard events from the user */
135
161
private initKeyboardEvents ( ) {
136
162
if ( ! process . stdin . isTTY ) {
@@ -149,46 +175,23 @@ class Top extends React.PureComponent<Props, State> {
149
175
} else {
150
176
switch ( key . name ) {
151
177
case "escape" :
152
- this . setState ( { selectedGroupIdx : - 1 } )
178
+ this . clearCurrentJobSelection ( )
153
179
break
180
+
154
181
case "up" :
155
182
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" )
166
189
break
167
190
168
191
case "left" :
169
192
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 )
180
194
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 */
192
195
}
193
196
}
194
197
} )
@@ -198,28 +201,38 @@ class Top extends React.PureComponent<Props, State> {
198
201
return { min : { cpu : 0 , mem : 0 , gpu : 0 } , tot : { } }
199
202
}
200
203
201
- private reinit ( context : Context ) {
204
+ private async reinit ( context : Context ) {
202
205
if ( this . state ?. watcher ) {
203
206
this . state ?. watcher . kill ( )
204
207
}
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
+ } )
207
214
}
208
215
209
216
/** We have received data from the controller */
210
- private readonly onData = ( rawModel : UpdatePayload ) => {
217
+ private readonly onData = ( rawModel : UpdatePayloadOrError ) => {
211
218
if ( rawModel . cluster !== this . currentContext . cluster || rawModel . namespace !== this . currentContext . namespace ) {
212
219
// this is straggler data from the prior context
213
220
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 } )
221
225
}
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
+ }
223
236
}
224
237
225
238
private groupBy ( model : UpdatePayload ) : State [ "groups" ] {
@@ -272,7 +285,9 @@ class Top extends React.PureComponent<Props, State> {
272
285
}
273
286
274
287
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 ) {
276
291
return < Text > No active jobs</ Text >
277
292
} else {
278
293
return (
@@ -291,13 +306,13 @@ class Top extends React.PureComponent<Props, State> {
291
306
}
292
307
293
308
public render ( ) {
294
- if ( ! this . state ?. groups ) {
309
+ if ( ! this . state ?. updateError && ! this . state ?. groups ) {
295
310
// TODO spinner? this means we haven't received the first data set, yet
296
311
return < React . Fragment />
297
312
} else {
298
313
return (
299
314
< Box flexDirection = "column" >
300
- < Header cluster = { this . state . rawModel . cluster } namespace = { this . state . rawModel . namespace } />
315
+ < Header { ... this . currentContext } />
301
316
< Box marginTop = { 1 } > { this . body ( ) } </ Box >
302
317
</ Box >
303
318
)
0 commit comments