Skip to content

Commit 80ad621

Browse files
authored
feat(devbox): added state transition display to detail page (#34)
## Description <!-- Provide a brief description of your changes --> **Note:** PR titles should follow [Conventional Commits](https://www.conventionalcommits.org/) format (e.g., `feat(devbox): add support for custom env vars` or `fix(snapshot): resolve pagination issue`) as they are used for automatic release notes generation. ## Type of Change <!-- Mark the relevant option with an 'x' --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test updates ## Related Issues <!-- Link to related issues using #issue-number --> Closes # ## Changes Made <!-- Describe the changes in detail --> ## Testing <!-- Describe how you tested your changes --> - [ ] I have tested locally - [ ] I have added/updated tests - [ ] All existing tests pass ## Checklist - [ ] My code follows the code style of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published ## Screenshots (if applicable) <!-- Add screenshots to help explain your changes --> ## Additional Notes <!-- Any additional information that reviewers should know -->
1 parent 802cc50 commit 80ad621

3 files changed

Lines changed: 136 additions & 1 deletion

File tree

src/components/DevboxDetailPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { StatusBadge } from "./StatusBadge.js";
66
import { MetadataDisplay } from "./MetadataDisplay.js";
77
import { Breadcrumb } from "./Breadcrumb.js";
88
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
9+
import { StateHistory } from "./StateHistory.js";
910
import { getDevboxUrl } from "../utils/url.js";
1011
import { colors } from "../utils/theme.js";
1112
import { useViewportHeight } from "../hooks/useViewportHeight.js";
@@ -840,6 +841,9 @@ export const DevboxDetailPage = ({
840841
</Box>
841842
)}
842843

844+
{/* State History */}
845+
<StateHistory stateTransitions={selectedDevbox.state_transitions} />
846+
843847
{/* Operations - inline display */}
844848
<Box flexDirection="column" marginTop={1}>
845849
<Text color={colors.primary} bold>

src/components/StateHistory.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import React from "react";
2+
import { Box, Text } from "ink";
3+
import figures from "figures";
4+
import { colors } from "../utils/theme.js";
5+
6+
interface StateHistoryProps {
7+
stateTransitions?: any;
8+
}
9+
10+
// Format time ago in a succinct way
11+
const formatTimeAgo = (timestamp: number): string => {
12+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
13+
14+
if (seconds < 60) return `${seconds}s ago`;
15+
16+
const minutes = Math.floor(seconds / 60);
17+
if (minutes < 60) return `${minutes}m ago`;
18+
19+
const hours = Math.floor(minutes / 60);
20+
if (hours < 24) return `${hours}h ago`;
21+
22+
const days = Math.floor(hours / 24);
23+
if (days < 30) return `${days}d ago`;
24+
25+
const months = Math.floor(days / 30);
26+
if (months < 12) return `${months}mo ago`;
27+
28+
const years = Math.floor(months / 12);
29+
return `${years}y ago`;
30+
};
31+
32+
// Format duration in a succinct way
33+
const formatDuration = (milliseconds: number): string => {
34+
const seconds = Math.floor(milliseconds / 1000);
35+
if (seconds < 60) return `${seconds}s`;
36+
37+
const minutes = Math.floor(seconds / 60);
38+
if (minutes < 60) return `${minutes}m`;
39+
40+
const hours = Math.floor(minutes / 60);
41+
if (hours < 24) return `${hours}h ${minutes % 60}m`;
42+
43+
const days = Math.floor(hours / 24);
44+
return `${days}d ${hours % 24}h`;
45+
};
46+
47+
// Capitalize first letter of a string
48+
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
49+
50+
export const StateHistory = ({ stateTransitions }: StateHistoryProps) => {
51+
if (
52+
!stateTransitions ||
53+
!Array.isArray(stateTransitions) ||
54+
stateTransitions.length === 0
55+
) {
56+
return null;
57+
}
58+
59+
// Get last 3 transitions (most recent first)
60+
const lastThree = (
61+
stateTransitions as Array<{
62+
status: string;
63+
transition_time_ms?: number;
64+
}>
65+
)
66+
.slice(-3)
67+
.reverse()
68+
.map((transition, idx, arr) => {
69+
const transitionTime = transition.transition_time_ms;
70+
// Calculate duration: time until next transition, or until now if it's the current state
71+
let duration = 0;
72+
if (transitionTime) {
73+
if (idx === 0) {
74+
// Most recent state - duration is from transition time to now
75+
duration = Date.now() - transitionTime;
76+
} else {
77+
// Previous state - duration is from this transition to the next one
78+
const nextTransition = arr[idx - 1];
79+
const nextTransitionTime = nextTransition.transition_time_ms;
80+
if (nextTransitionTime) {
81+
duration = nextTransitionTime - transitionTime;
82+
}
83+
}
84+
}
85+
return {
86+
status: transition.status,
87+
transitionTime,
88+
duration,
89+
};
90+
})
91+
.filter((state) => state.transitionTime); // Only show states with valid timestamps
92+
93+
if (lastThree.length === 0) {
94+
return null;
95+
}
96+
97+
return (
98+
<Box flexDirection="column" marginBottom={1} paddingX={1}>
99+
<Text color={colors.info} bold>
100+
{figures.circleFilled} State History
101+
</Text>
102+
<Box flexDirection="column">
103+
{lastThree.map((state, idx) => (
104+
<Box key={idx} flexDirection="column">
105+
<Text dimColor>
106+
{capitalize(state.status)}
107+
{state.transitionTime && (
108+
<>
109+
{" "}
110+
at {new Date(state.transitionTime).toLocaleString()}{" "}
111+
<Text color={colors.textDim} dimColor>
112+
({formatTimeAgo(state.transitionTime)})
113+
</Text>
114+
{state.duration > 0 && (
115+
<>
116+
{" "}
117+
• Duration:{" "}
118+
<Text color={colors.info}>
119+
{formatDuration(state.duration)}
120+
</Text>
121+
</>
122+
)}
123+
</>
124+
)}
125+
</Text>
126+
</Box>
127+
))}
128+
</Box>
129+
</Box>
130+
);
131+
};

src/utils/theme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const darkColors: ColorPalette = {
4343

4444
// UI colors
4545
text: "#FFFFFF", // White
46-
textDim: "#9CA3AF", // Gray
46+
textDim: "#B8C2D0", // Brighter gray for better visibility in dark mode
4747
border: "#6B7280", // Medium gray
4848
background: "#000000", // Black
4949

0 commit comments

Comments
 (0)