Skip to content

Commit e22b01d

Browse files
committed
cp dines
1 parent 7f3db2f commit e22b01d

13 files changed

Lines changed: 715 additions & 350 deletions

src/components/DevboxCreatePage.tsx

Lines changed: 203 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import { SuccessMessage } from "./SuccessMessage.js";
1414
import { Breadcrumb } from "./Breadcrumb.js";
1515
import { NavigationTips } from "./NavigationTips.js";
1616
import { 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";
1820
import {
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

Comments
 (0)