feat(deployments): Browse container files and bring them onto the host#81
Conversation
A deployment's own directory is browsable and editable, but whatever the running service keeps to itself is not, and that is usually where the configuration a user wants to read lives. Today the only way to it is a terminal. The new tab lists what a running service holds and can bring any path onto the host, after which the existing Files tab edits it and the container reads the same content. This is the opposite direction to mounting a host path into a container, which hides whatever was there. Browsing needs the service running and an image that ships a shell, since the engine cannot list a directory. That case is explained rather than left as a bare failure.
Container files were their own tab, which split browsing across two places when the two are the same task: find a path, then read or edit it. Files now switches between the host and a running service, and bringing a path out of the container drops straight back to the host view where it can be edited. The panel had also styled itself, so it sat flush against the tab's edges and drew colours the theme does not set, which left it looking unlike the rest of the app in either theme. It uses the design tokens now, and scrolls a long directory inside the pane rather than stretching the tab.
The switcher had space above it and none below, so it sat high in its row rather than centred, and its row did not line up with the file browser's toolbar directly beneath it.
A path a container reads through a bind mount looked like any other file, so there was nothing to say that editing or deleting it reaches inside the running container, and no way to disconnect it short of editing the compose file by hand. Mounted paths now carry a tag naming what reads them and where, and can be unmounted from the row menu. The warnings follow what actually happens: deleting a mounted path takes it from the container too, because the host copy is what the container reads; unmounting first sends the service back to its image's content and leaves the copy on the host, where deleting it is then harmless.
Code Review SummaryThis PR introduces a significant feature allowing users to browse files inside running containers and 'materialize' them onto the host for editing. It includes a new 🚀 Key Improvements
💡 Minor Suggestions
|
| } | ||
| }; | ||
|
|
||
| const formatBytes = (bytes: number) => { |
There was a problem hiding this comment.
The Math.log(0) case is mathematically undefined and Math.log(bytes) will return -Infinity if bytes is 0. While the if (!bytes) check at the start handles 0, it won't handle cases where bytes is less than 1 (though rare for file sizes). A more robust approach ensures the index is at least 0.
| const formatBytes = (bytes: number) => { | |
| const formatBytes = (bytes: number) => { | |
| if (bytes <= 0) return "0 B"; | |
| const units = ["B", "KB", "MB", "GB"]; | |
| const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)); | |
| return `${(bytes / Math.pow(1024, i)).toFixed(i ? 1 : 0)} ${units[i]}`; | |
| }; |
| try { | ||
| // A path can be mounted into more than one service, and it stays mounted | ||
| // until the last one lets go. | ||
| for (const mount of mounts) { |
There was a problem hiding this comment.
When unmounting a path that is used by multiple services, perform the deletions sequentially with better error handling, or ideally use Promise.allSettled if you want to attempt all even if one fails. Currently, a single failure stops the loop, potentially leaving the UI in an inconsistent state regarding what is actually mounted.
| for (const mount of mounts) { | |
| const results = await Promise.allSettled( | |
| mounts.map((mount) => | |
| deploymentsApi.removeComposeMount(props.deploymentName, { | |
| source_path: mount.source, | |
| target_path: mount.target, | |
| service_name: mount.service, | |
| }) | |
| ) | |
| ); | |
| const failed = results.filter((r) => r.status === "rejected"); | |
| if (failed.length > 0) throw new Error(`Failed to remove ${failed.length} mount(s)`); |
The UI side of flatrun/agent#139. Files could browse and edit a deployment's own directory but not what the running container holds, which is usually where the configuration a user wants lives. The only way to it was a terminal.
Files now switches between the host and a running service. A path brought out of the container lands back in the host view where the existing editor already works, so there is no second editor.
The opposite direction already existed, mounting a host path into a container, which hides whatever the container had there. That asymmetry is what made the container side hard to reach.
Mounted paths carry a tag naming what reads them and where. Their warnings follow what happens: deleting a mounted path takes it from the running container, because the host copy is what the container reads; unmounting first returns the service to its image's content and leaves the copy on the host, where deleting it is harmless.
Browsing needs the service running and an image that ships a shell, since the engine cannot list a directory.
Depends on flatrun/agent#172 for its endpoints.