@@ -3,10 +3,14 @@ import { Box, Text, useInput, useApp } from "ink";
33import figures from "figures" ;
44import type { DiskSnapshotsCursorIDPage } from "@runloop/api-client/pagination" ;
55import { getClient } from "../../utils/client.js" ;
6+ import { Header } from "../../components/Header.js" ;
67import { SpinnerComponent } from "../../components/Spinner.js" ;
78import { ErrorMessage } from "../../components/ErrorMessage.js" ;
9+ import { SuccessMessage } from "../../components/SuccessMessage.js" ;
810import { Breadcrumb } from "../../components/Breadcrumb.js" ;
911import { Table , createTextColumn } from "../../components/Table.js" ;
12+ import { ActionsPopup } from "../../components/ActionsPopup.js" ;
13+ import { Operation } from "../../components/OperationsMenu.js" ;
1014import { formatTimeAgo } from "../../components/ResourceListView.js" ;
1115import { createExecutor } from "../../utils/CommandExecutor.js" ;
1216import { colors } from "../../utils/theme.js" ;
@@ -32,6 +36,14 @@ const ListSnapshotsUI = ({
3236} ) => {
3337 const { exit : inkExit } = useApp ( ) ;
3438 const [ selectedIndex , setSelectedIndex ] = React . useState ( 0 ) ;
39+ const [ showPopup , setShowPopup ] = React . useState ( false ) ;
40+ const [ selectedOperation , setSelectedOperation ] = React . useState ( 0 ) ;
41+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42+ const [ selectedSnapshot , setSelectedSnapshot ] = React . useState < any | null > ( null ) ;
43+ const [ executingOperation , setExecutingOperation ] = React . useState < string | null > ( null ) ;
44+ const [ operationResult , setOperationResult ] = React . useState < string | null > ( null ) ;
45+ const [ operationError , setOperationError ] = React . useState < Error | null > ( null ) ;
46+ const [ operationLoading , setOperationLoading ] = React . useState ( false ) ;
3547
3648 // Calculate overhead for viewport height
3749 const overhead = 13 ;
@@ -47,7 +59,7 @@ const ListSnapshotsUI = ({
4759 const nameWidth = Math . max ( 15 , terminalWidth >= 120 ? 30 : 25 ) ;
4860 const devboxWidth = 15 ;
4961 const timeWidth = 20 ;
50- const showDevboxId = terminalWidth >= 100 && ! devboxId ;
62+ const showDevboxIdColumn = terminalWidth >= 100 && ! devboxId ;
5163
5264 // Fetch function for pagination hook
5365 const fetchPage = React . useCallback (
@@ -117,9 +129,23 @@ const ListSnapshotsUI = ({
117129 pageSize : PAGE_SIZE ,
118130 getItemId : ( snapshot : any ) => snapshot . id ,
119131 pollInterval : 2000 ,
132+ pollingEnabled : ! showPopup && ! executingOperation ,
120133 deps : [ devboxId , PAGE_SIZE ] ,
121134 } ) ;
122135
136+ // Operations for snapshots
137+ const operations : Operation [ ] = React . useMemo (
138+ ( ) => [
139+ {
140+ key : "delete" ,
141+ label : "Delete Snapshot" ,
142+ color : colors . error ,
143+ icon : figures . cross ,
144+ } ,
145+ ] ,
146+ [ ] ,
147+ ) ;
148+
123149 // Build columns
124150 const columns = React . useMemo (
125151 ( ) => [
@@ -146,7 +172,7 @@ const ListSnapshotsUI = ({
146172 color : colors . idColor ,
147173 dimColor : false ,
148174 bold : false ,
149- visible : showDevboxId ,
175+ visible : showDevboxIdColumn ,
150176 } ,
151177 ) ,
152178 createTextColumn (
@@ -162,7 +188,7 @@ const ListSnapshotsUI = ({
162188 } ,
163189 ) ,
164190 ] ,
165- [ idWidth , nameWidth , devboxWidth , timeWidth , showDevboxId ] ,
191+ [ idWidth , nameWidth , devboxWidth , timeWidth , showDevboxIdColumn ] ,
166192 ) ;
167193
168194 // Handle Ctrl+C to exit
@@ -175,12 +201,72 @@ const ListSnapshotsUI = ({
175201 }
176202 } , [ snapshots . length , selectedIndex ] ) ;
177203
204+ const selectedSnapshotItem = snapshots [ selectedIndex ] ;
205+
178206 // Calculate pagination info for display
179207 const totalPages = Math . max ( 1 , Math . ceil ( totalCount / PAGE_SIZE ) ) ;
180208 const startIndex = currentPage * PAGE_SIZE ;
181209 const endIndex = startIndex + snapshots . length ;
182210
211+ const executeOperation = async ( ) => {
212+ const client = getClient ( ) ;
213+ const snapshot = selectedSnapshot ;
214+
215+ if ( ! snapshot ) return ;
216+
217+ try {
218+ setOperationLoading ( true ) ;
219+ switch ( executingOperation ) {
220+ case "delete" :
221+ await client . devboxes . deleteDiskSnapshot ( snapshot . id ) ;
222+ setOperationResult ( `Snapshot ${ snapshot . id } deleted successfully` ) ;
223+ break ;
224+ }
225+ } catch ( err ) {
226+ setOperationError ( err as Error ) ;
227+ } finally {
228+ setOperationLoading ( false ) ;
229+ }
230+ } ;
231+
183232 useInput ( ( input , key ) => {
233+ // Handle operation result display
234+ if ( operationResult || operationError ) {
235+ if ( input === "q" || key . escape || key . return ) {
236+ setOperationResult ( null ) ;
237+ setOperationError ( null ) ;
238+ setExecutingOperation ( null ) ;
239+ setSelectedSnapshot ( null ) ;
240+ }
241+ return ;
242+ }
243+
244+ // Handle popup navigation
245+ if ( showPopup ) {
246+ if ( key . upArrow && selectedOperation > 0 ) {
247+ setSelectedOperation ( selectedOperation - 1 ) ;
248+ } else if ( key . downArrow && selectedOperation < operations . length - 1 ) {
249+ setSelectedOperation ( selectedOperation + 1 ) ;
250+ } else if ( key . return ) {
251+ setShowPopup ( false ) ;
252+ const operationKey = operations [ selectedOperation ] . key ;
253+ setSelectedSnapshot ( selectedSnapshotItem ) ;
254+ setExecutingOperation ( operationKey ) ;
255+ // Execute immediately after state update
256+ setTimeout ( ( ) => executeOperation ( ) , 0 ) ;
257+ } else if ( key . escape || input === "q" ) {
258+ setShowPopup ( false ) ;
259+ setSelectedOperation ( 0 ) ;
260+ } else if ( input === "d" ) {
261+ // Delete hotkey
262+ setShowPopup ( false ) ;
263+ setSelectedSnapshot ( selectedSnapshotItem ) ;
264+ setExecutingOperation ( "delete" ) ;
265+ setTimeout ( ( ) => executeOperation ( ) , 0 ) ;
266+ }
267+ return ;
268+ }
269+
184270 const pageSnapshots = snapshots . length ;
185271
186272 // Handle list view navigation
@@ -194,6 +280,9 @@ const ListSnapshotsUI = ({
194280 } else if ( ( input === "p" || key . leftArrow ) && ! loading && ! navigating && hasPrev ) {
195281 prevPage ( ) ;
196282 setSelectedIndex ( 0 ) ;
283+ } else if ( input === "a" && selectedSnapshotItem ) {
284+ setShowPopup ( true ) ;
285+ setSelectedOperation ( 0 ) ;
197286 } else if ( key . escape ) {
198287 if ( onBack ) {
199288 onBack ( ) ;
@@ -205,6 +294,57 @@ const ListSnapshotsUI = ({
205294 }
206295 } ) ;
207296
297+ // Operation result display
298+ if ( operationResult || operationError ) {
299+ const operationLabel =
300+ operations . find ( ( o ) => o . key === executingOperation ) ?. label || "Operation" ;
301+ return (
302+ < >
303+ < Breadcrumb
304+ items = { [
305+ { label : "Snapshots" } ,
306+ { label : selectedSnapshot ?. name || selectedSnapshot ?. id || "Snapshot" } ,
307+ { label : operationLabel , active : true } ,
308+ ] }
309+ />
310+ < Header title = "Operation Result" />
311+ { operationResult && < SuccessMessage message = { operationResult } /> }
312+ { operationError && (
313+ < ErrorMessage message = "Operation failed" error = { operationError } />
314+ ) }
315+ < Box marginTop = { 1 } >
316+ < Text color = { colors . textDim } dimColor >
317+ Press [Enter], [q], or [esc] to continue
318+ </ Text >
319+ </ Box >
320+ </ >
321+ ) ;
322+ }
323+
324+ // Operation loading state
325+ if ( operationLoading && selectedSnapshot ) {
326+ const operationLabel =
327+ operations . find ( ( o ) => o . key === executingOperation ) ?. label || "Operation" ;
328+ const messages : Record < string , string > = {
329+ delete : "Deleting snapshot..." ,
330+ } ;
331+ return (
332+ < >
333+ < Breadcrumb
334+ items = { [
335+ { label : "Snapshots" } ,
336+ { label : selectedSnapshot . name || selectedSnapshot . id } ,
337+ { label : operationLabel , active : true } ,
338+ ] }
339+ />
340+ < Header title = "Executing Operation" />
341+ < SpinnerComponent
342+ message = { messages [ executingOperation as string ] || "Please wait..." }
343+ />
344+ </ >
345+ ) ;
346+ }
347+
208348 // Loading state
209349 if ( loading && snapshots . length === 0 ) {
210350 return (
@@ -266,49 +406,71 @@ const ListSnapshotsUI = ({
266406 ] }
267407 />
268408
269- { /* Table */ }
270- < Table
271- data = { snapshots }
272- keyExtractor = { ( snapshot : any ) => snapshot . id }
273- selectedIndex = { selectedIndex }
274- title = { `snapshots[${ totalCount } ]` }
275- columns = { columns }
276- />
409+ { /* Table - hide when popup is shown */ }
410+ { ! showPopup && (
411+ < Table
412+ data = { snapshots }
413+ keyExtractor = { ( snapshot : any ) => snapshot . id }
414+ selectedIndex = { selectedIndex }
415+ title = { `snapshots[${ totalCount } ]` }
416+ columns = { columns }
417+ />
418+ ) }
277419
278- { /* Statistics Bar */ }
279- < Box marginTop = { 1 } paddingX = { 1 } >
280- < Text color = { colors . primary } bold >
281- { figures . hamburger } { totalCount }
282- </ Text >
283- < Text color = { colors . textDim } dimColor >
284- { " " }
285- total
286- </ Text >
287- { totalPages > 1 && (
288- < >
289- < Text color = { colors . textDim } dimColor >
290- { " " }
291- •{ " " }
292- </ Text >
293- { navigating ? (
294- < Text color = { colors . warning } >
295- { figures . pointer } Loading page { currentPage + 1 } ...
296- </ Text >
297- ) : (
420+ { /* Statistics Bar - hide when popup is shown */ }
421+ { ! showPopup && (
422+ < Box marginTop = { 1 } paddingX = { 1 } >
423+ < Text color = { colors . primary } bold >
424+ { figures . hamburger } { totalCount }
425+ </ Text >
426+ < Text color = { colors . textDim } dimColor >
427+ { " " }
428+ total
429+ </ Text >
430+ { totalPages > 1 && (
431+ < >
298432 < Text color = { colors . textDim } dimColor >
299- Page { currentPage + 1 } of { totalPages }
433+ { " " }
434+ •{ " " }
300435 </ Text >
301- ) }
302- </ >
303- ) }
304- < Text color = { colors . textDim } dimColor >
305- { " " }
306- •{ " " }
307- </ Text >
308- < Text color = { colors . textDim } dimColor >
309- Showing { startIndex + 1 } -{ endIndex } of { totalCount }
310- </ Text >
311- </ Box >
436+ { navigating ? (
437+ < Text color = { colors . warning } >
438+ { figures . pointer } Loading page { currentPage + 1 } ...
439+ </ Text >
440+ ) : (
441+ < Text color = { colors . textDim } dimColor >
442+ Page { currentPage + 1 } of { totalPages }
443+ </ Text >
444+ ) }
445+ </ >
446+ ) }
447+ < Text color = { colors . textDim } dimColor >
448+ { " " }
449+ •{ " " }
450+ </ Text >
451+ < Text color = { colors . textDim } dimColor >
452+ Showing { startIndex + 1 } -{ endIndex } of { totalCount }
453+ </ Text >
454+ </ Box >
455+ ) }
456+
457+ { /* Actions Popup */ }
458+ { showPopup && selectedSnapshotItem && (
459+ < Box marginTop = { 2 } justifyContent = "center" >
460+ < ActionsPopup
461+ devbox = { selectedSnapshotItem }
462+ operations = { operations . map ( ( op ) => ( {
463+ key : op . key ,
464+ label : op . label ,
465+ color : op . color ,
466+ icon : op . icon ,
467+ shortcut : op . key === "delete" ? "d" : "" ,
468+ } ) ) }
469+ selectedOperation = { selectedOperation }
470+ onClose = { ( ) => setShowPopup ( false ) }
471+ />
472+ </ Box >
473+ ) }
312474
313475 { /* Help Bar */ }
314476 < Box marginTop = { 1 } paddingX = { 1 } >
@@ -323,6 +485,10 @@ const ListSnapshotsUI = ({
323485 { figures . arrowRight } Page
324486 </ Text >
325487 ) }
488+ < Text color = { colors . textDim } dimColor >
489+ { " " }
490+ • [a] Actions
491+ </ Text >
326492 < Text color = { colors . textDim } dimColor >
327493 { " " }
328494 • [Esc] Back
0 commit comments