@@ -14,7 +14,9 @@ import { SuccessMessage } from "./SuccessMessage.js";
1414import { Breadcrumb } from "./Breadcrumb.js" ;
1515import { NavigationTips } from "./NavigationTips.js" ;
1616import { MetadataDisplay } from "./MetadataDisplay.js" ;
17- import { ResourcePicker } from "./ResourcePicker.js" ;
17+ import { ResourcePicker , createTextColumn , Column } from "./ResourcePicker.js" ;
18+ import { formatTimeAgo } from "./ResourceListView.js" ;
19+ import { getStatusDisplay } from "./StatusBadge.js" ;
1820import {
1921 FormTextInput ,
2022 FormSelect ,
@@ -116,17 +118,21 @@ export const DevboxCreatePage = ({
116118 const [ creating , setCreating ] = React . useState ( false ) ;
117119 const [ result , setResult ] = React . useState < DevboxView | null > ( null ) ;
118120 const [ error , setError ] = React . useState < Error | null > ( null ) ;
119-
121+
120122 // Source picker states (toggle between blueprint/snapshot)
121- const [ sourceTypeToggle , setSourceTypeToggle ] = React . useState < "blueprint" | "snapshot" > (
122- initialSnapshotId ? "snapshot" : "blueprint "
123- ) ;
123+ const [ sourceTypeToggle , setSourceTypeToggle ] = React . useState <
124+ "blueprint" | "snapshot "
125+ > ( initialSnapshotId ? "snapshot" : "blueprint" ) ;
124126 const [ showBlueprintPicker , setShowBlueprintPicker ] = React . useState ( false ) ;
125127 const [ showSnapshotPicker , setShowSnapshotPicker ] = React . useState ( false ) ;
126- const [ showNetworkPolicyPicker , setShowNetworkPolicyPicker ] = React . useState ( false ) ;
127- const [ selectedBlueprintName , setSelectedBlueprintName ] = React . useState < string > ( "" ) ;
128- const [ selectedSnapshotName , setSelectedSnapshotName ] = React . useState < string > ( "" ) ;
129- const [ selectedNetworkPolicyName , setSelectedNetworkPolicyName ] = React . useState < string > ( "" ) ;
128+ const [ showNetworkPolicyPicker , setShowNetworkPolicyPicker ] =
129+ React . useState ( false ) ;
130+ const [ selectedBlueprintName , setSelectedBlueprintName ] =
131+ React . useState < string > ( "" ) ;
132+ const [ selectedSnapshotName , setSelectedSnapshotName ] =
133+ React . useState < string > ( "" ) ;
134+ const [ selectedNetworkPolicyName , setSelectedNetworkPolicyName ] =
135+ React . useState < string > ( "" ) ;
130136
131137 const baseFields : Array < {
132138 key : FormField ;
@@ -282,7 +288,7 @@ export const DevboxCreatePage = ({
282288 // If something is already selected, open that type's picker to change it
283289 const hasBlueprint = ! ! ( selectedBlueprintName || formData . blueprint_id ) ;
284290 const hasSnapshot = ! ! ( selectedSnapshotName || formData . snapshot_id ) ;
285-
291+
286292 if ( hasBlueprint ) {
287293 setShowBlueprintPicker ( true ) ;
288294 } else if ( hasSnapshot ) {
@@ -297,7 +303,7 @@ export const DevboxCreatePage = ({
297303 }
298304 return ;
299305 }
300-
306+
301307 // Delete key on source field to clear selection
302308 if ( currentField === "source" && ( input === "d" || key . delete ) ) {
303309 handleClearSource ( ) ;
@@ -333,14 +339,24 @@ export const DevboxCreatePage = ({
333339 return ;
334340 }
335341 } ,
336- { isActive : ! inMetadataSection && ! showBlueprintPicker && ! showSnapshotPicker && ! showNetworkPolicyPicker } ,
342+ {
343+ isActive :
344+ ! inMetadataSection &&
345+ ! showBlueprintPicker &&
346+ ! showSnapshotPicker &&
347+ ! showNetworkPolicyPicker ,
348+ } ,
337349 ) ;
338350
339351 // Handle blueprint selection
340352 const handleBlueprintSelect = React . useCallback ( ( blueprints : Blueprint [ ] ) => {
341353 if ( blueprints . length > 0 ) {
342354 const blueprint = blueprints [ 0 ] ;
343- setFormData ( ( prev ) => ( { ...prev , blueprint_id : blueprint . id , snapshot_id : "" } ) ) ;
355+ setFormData ( ( prev ) => ( {
356+ ...prev ,
357+ blueprint_id : blueprint . id ,
358+ snapshot_id : "" ,
359+ } ) ) ;
344360 setSelectedBlueprintName ( blueprint . name || blueprint . id ) ;
345361 setSelectedSnapshotName ( "" ) ;
346362 }
@@ -351,22 +367,29 @@ export const DevboxCreatePage = ({
351367 const handleSnapshotSelect = React . useCallback ( ( snapshots : Snapshot [ ] ) => {
352368 if ( snapshots . length > 0 ) {
353369 const snapshot = snapshots [ 0 ] ;
354- setFormData ( ( prev ) => ( { ...prev , snapshot_id : snapshot . id , blueprint_id : "" } ) ) ;
370+ setFormData ( ( prev ) => ( {
371+ ...prev ,
372+ snapshot_id : snapshot . id ,
373+ blueprint_id : "" ,
374+ } ) ) ;
355375 setSelectedSnapshotName ( snapshot . name || snapshot . id ) ;
356376 setSelectedBlueprintName ( "" ) ;
357377 }
358378 setShowSnapshotPicker ( false ) ;
359379 } , [ ] ) ;
360380
361381 // Handle network policy selection
362- const handleNetworkPolicySelect = React . useCallback ( ( policies : NetworkPolicy [ ] ) => {
363- if ( policies . length > 0 ) {
364- const policy = policies [ 0 ] ;
365- setFormData ( ( prev ) => ( { ...prev , network_policy_id : policy . id } ) ) ;
366- setSelectedNetworkPolicyName ( policy . name || policy . id ) ;
367- }
368- setShowNetworkPolicyPicker ( false ) ;
369- } , [ ] ) ;
382+ const handleNetworkPolicySelect = React . useCallback (
383+ ( policies : NetworkPolicy [ ] ) => {
384+ if ( policies . length > 0 ) {
385+ const policy = policies [ 0 ] ;
386+ setFormData ( ( prev ) => ( { ...prev , network_policy_id : policy . id } ) ) ;
387+ setSelectedNetworkPolicyName ( policy . name || policy . id ) ;
388+ }
389+ setShowNetworkPolicyPicker ( false ) ;
390+ } ,
391+ [ ] ,
392+ ) ;
370393
371394 // Handle clearing source
372395 const handleClearSource = React . useCallback ( ( ) => {
@@ -646,6 +669,67 @@ export const DevboxCreatePage = ({
646669
647670 // Blueprint picker screen
648671 if ( showBlueprintPicker ) {
672+ const blueprintColumns : Column < Blueprint > [ ] = [
673+ {
674+ key : "statusIcon" ,
675+ label : "" ,
676+ width : 2 ,
677+ render : ( blueprint , _index , isSelected ) => {
678+ const statusDisplay = getStatusDisplay ( blueprint . status || "" ) ;
679+ return (
680+ < Text
681+ color = { isSelected ? "white" : statusDisplay . color }
682+ bold = { true }
683+ inverse = { isSelected }
684+ wrap = "truncate"
685+ >
686+ { statusDisplay . icon } { " " }
687+ </ Text >
688+ ) ;
689+ } ,
690+ } ,
691+ createTextColumn < Blueprint > (
692+ "id" ,
693+ "ID" ,
694+ ( blueprint ) => blueprint . id ,
695+ { width : 25 , color : colors . idColor } ,
696+ ) ,
697+ createTextColumn < Blueprint > (
698+ "name" ,
699+ "Name" ,
700+ ( blueprint ) => blueprint . name || "" ,
701+ { width : 30 } ,
702+ ) ,
703+ {
704+ key : "status" ,
705+ label : "Status" ,
706+ width : 12 ,
707+ render : ( blueprint , _index , isSelected ) => {
708+ const statusDisplay = getStatusDisplay ( blueprint . status || "" ) ;
709+ const padded = statusDisplay . text . slice ( 0 , 12 ) . padEnd ( 12 , " " ) ;
710+ return (
711+ < Text
712+ color = { isSelected ? "white" : statusDisplay . color }
713+ bold = { true }
714+ inverse = { isSelected }
715+ wrap = "truncate"
716+ >
717+ { padded }
718+ </ Text >
719+ ) ;
720+ } ,
721+ } ,
722+ createTextColumn < Blueprint > (
723+ "created" ,
724+ "Created" ,
725+ ( blueprint ) => blueprint . create_time_ms ? formatTimeAgo ( blueprint . create_time_ms ) : "" ,
726+ { width : 18 , color : colors . textDim } ,
727+ ) ,
728+ ] ;
729+
730+ // Filter out failed blueprints
731+ const failedStatuses = [ "failure" , "build_failed" , "failed" ] ;
732+
649733 return (
650734 < ResourcePicker < Blueprint >
651735 config = { {
@@ -656,17 +740,21 @@ export const DevboxCreatePage = ({
656740 startingAfter : params . startingAt ,
657741 search : params . search ,
658742 } ) ;
743+ // Filter out failed blueprints
744+ const validBlueprints = result . blueprints . filter (
745+ ( bp ) => ! failedStatuses . includes ( bp . status || "" )
746+ ) ;
659747 return {
660- items : result . blueprints ,
748+ items : validBlueprints ,
661749 hasMore : result . hasMore ,
662- totalCount : result . totalCount ,
750+ totalCount : validBlueprints . length ,
663751 } ;
664752 } ,
665753 getItemId : ( blueprint ) => blueprint . id ,
666754 getItemLabel : ( blueprint ) => blueprint . name || blueprint . id ,
667- getItemStatus : ( blueprint ) => blueprint . status ,
755+ columns : blueprintColumns ,
668756 mode : "single" ,
669- emptyMessage : "No blueprints found" ,
757+ emptyMessage : "No blueprints found (failed blueprints are hidden) " ,
670758 searchPlaceholder : "Search blueprints..." ,
671759 breadcrumbItems : [
672760 { label : "Devboxes" } ,
@@ -683,6 +771,33 @@ export const DevboxCreatePage = ({
683771
684772 // Snapshot picker screen
685773 if ( showSnapshotPicker ) {
774+ const snapshotColumns : Column < Snapshot > [ ] = [
775+ createTextColumn < Snapshot > (
776+ "id" ,
777+ "ID" ,
778+ ( snapshot ) => snapshot . id ,
779+ { width : 25 , color : colors . idColor } ,
780+ ) ,
781+ createTextColumn < Snapshot > (
782+ "name" ,
783+ "Name" ,
784+ ( snapshot ) => snapshot . name || "" ,
785+ { width : 30 } ,
786+ ) ,
787+ createTextColumn < Snapshot > (
788+ "status" ,
789+ "Status" ,
790+ ( snapshot ) => snapshot . status || "" ,
791+ { width : 12 } ,
792+ ) ,
793+ createTextColumn < Snapshot > (
794+ "created" ,
795+ "Created" ,
796+ ( snapshot ) => snapshot . create_time_ms ? formatTimeAgo ( snapshot . create_time_ms ) : "" ,
797+ { width : 18 , color : colors . textDim } ,
798+ ) ,
799+ ] ;
800+
686801 return (
687802 < ResourcePicker < Snapshot >
688803 config = { {
@@ -700,7 +815,7 @@ export const DevboxCreatePage = ({
700815 } ,
701816 getItemId : ( snapshot ) => snapshot . id ,
702817 getItemLabel : ( snapshot ) => snapshot . name || snapshot . id ,
703- getItemStatus : ( snapshot ) => snapshot . status ,
818+ columns : snapshotColumns ,
704819 mode : "single" ,
705820 emptyMessage : "No snapshots found" ,
706821 searchPlaceholder : "Search snapshots..." ,
@@ -719,6 +834,40 @@ export const DevboxCreatePage = ({
719834
720835 // Network policy picker screen
721836 if ( showNetworkPolicyPicker ) {
837+ // Helper to get egress type label
838+ const getEgressLabel = ( egress : NetworkPolicy [ "egress" ] ) => {
839+ if ( egress . allow_all ) return "Allow All" ;
840+ if ( egress . allowed_hostnames ?. length === 0 ) return "Deny All" ;
841+ return `Custom (${ egress . allowed_hostnames ?. length || 0 } )` ;
842+ } ;
843+
844+ const networkPolicyColumns : Column < NetworkPolicy > [ ] = [
845+ createTextColumn < NetworkPolicy > (
846+ "id" ,
847+ "ID" ,
848+ ( policy ) => policy . id ,
849+ { width : 25 , color : colors . idColor } ,
850+ ) ,
851+ createTextColumn < NetworkPolicy > (
852+ "name" ,
853+ "Name" ,
854+ ( policy ) => policy . name || "" ,
855+ { width : 25 } ,
856+ ) ,
857+ createTextColumn < NetworkPolicy > (
858+ "egress" ,
859+ "Egress" ,
860+ ( policy ) => getEgressLabel ( policy . egress ) ,
861+ { width : 15 } ,
862+ ) ,
863+ createTextColumn < NetworkPolicy > (
864+ "created" ,
865+ "Created" ,
866+ ( policy ) => policy . create_time_ms ? formatTimeAgo ( policy . create_time_ms ) : "" ,
867+ { width : 18 , color : colors . textDim } ,
868+ ) ,
869+ ] ;
870+
722871 return (
723872 < ResourcePicker < NetworkPolicy >
724873 config = { {
@@ -737,6 +886,7 @@ export const DevboxCreatePage = ({
737886 } ,
738887 getItemId : ( policy ) => policy . id ,
739888 getItemLabel : ( policy ) => policy . name || policy . id ,
889+ columns : networkPolicyColumns ,
740890 mode : "single" ,
741891 emptyMessage : "No network policies found" ,
742892 searchPlaceholder : "Search network policies..." ,
@@ -748,7 +898,9 @@ export const DevboxCreatePage = ({
748898 } }
749899 onSelect = { handleNetworkPolicySelect }
750900 onCancel = { ( ) => setShowNetworkPolicyPicker ( false ) }
751- initialSelected = { formData . network_policy_id ? [ formData . network_policy_id ] : [ ] }
901+ initialSelected = {
902+ formData . network_policy_id ? [ formData . network_policy_id ] : [ ]
903+ }
752904 />
753905 ) ;
754906 }
@@ -812,16 +964,20 @@ export const DevboxCreatePage = ({
812964
813965 if ( field . type === "source" ) {
814966 // Check if either blueprint or snapshot is selected
815- const selectedBlueprintValue = selectedBlueprintName || formData . blueprint_id ;
816- const selectedSnapshotValue = selectedSnapshotName || formData . snapshot_id ;
967+ const selectedBlueprintValue =
968+ selectedBlueprintName || formData . blueprint_id ;
969+ const selectedSnapshotValue =
970+ selectedSnapshotName || formData . snapshot_id ;
817971 const hasBlueprint = ! ! selectedBlueprintValue ;
818972 const hasSnapshot = ! ! selectedSnapshotValue ;
819973 const hasSelection = hasBlueprint || hasSnapshot ;
820974
821975 // If something is selected, show it clearly with its type
822976 if ( hasSelection ) {
823977 const selectedType = hasBlueprint ? "Blueprint" : "Snapshot" ;
824- const selectedValue = hasBlueprint ? selectedBlueprintValue : selectedSnapshotValue ;
978+ const selectedValue = hasBlueprint
979+ ? selectedBlueprintValue
980+ : selectedSnapshotValue ;
825981
826982 return (
827983 < Box key = { field . key } marginBottom = { 0 } >
@@ -832,7 +988,8 @@ export const DevboxCreatePage = ({
832988 < Text color = { colors . idColor } > { selectedValue } </ Text >
833989 { isActive && (
834990 < Text color = { colors . textDim } dimColor >
835- { " " } [Enter to change, d to clear]
991+ { " " }
992+ [Enter to change, d to clear]
836993 </ Text >
837994 ) }
838995 </ Box >
@@ -850,24 +1007,34 @@ export const DevboxCreatePage = ({
8501007 { isActive ? figures . arrowLeft : "" } { " " }
8511008 </ Text >
8521009 < Text
853- color = { sourceTypeToggle === "blueprint" ? colors . primary : colors . textDim }
1010+ color = {
1011+ sourceTypeToggle === "blueprint"
1012+ ? colors . primary
1013+ : colors . textDim
1014+ }
8541015 bold = { sourceTypeToggle === "blueprint" }
8551016 >
8561017 Blueprint
8571018 </ Text >
8581019 < Text color = { colors . textDim } > / </ Text >
8591020 < Text
860- color = { sourceTypeToggle === "snapshot" ? colors . primary : colors . textDim }
1021+ color = {
1022+ sourceTypeToggle === "snapshot"
1023+ ? colors . primary
1024+ : colors . textDim
1025+ }
8611026 bold = { sourceTypeToggle === "snapshot" }
8621027 >
8631028 Snapshot
8641029 </ Text >
8651030 < Text color = { isActive ? colors . text : colors . textDim } >
866- { " " } { isActive ? figures . arrowRight : "" }
1031+ { " " }
1032+ { isActive ? figures . arrowRight : "" }
8671033 </ Text >
8681034 { isActive && (
8691035 < Text color = { colors . textDim } dimColor >
870- { " " } [Enter to select]
1036+ { " " }
1037+ [Enter to select]
8711038 </ Text >
8721039 ) }
8731040 </ Box >
0 commit comments