fix(ui): improve tree view layout and icon ordering
- Swap icon order: HTTP status first, container icon next to domain name - Use ├─ / └─ connectors on image subtitle lines - Use ├─ when node has children below, └─ when it's the last item - Remove blank lines between top-level items for compact output
- commit
- d4ce71df827595160dbad9196eae88d6f807a19b
- author
- brice · · 2026-03-24 06:23 UTC
- parent
- 6ea4218
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +15 -10
M internal/adapters/in/cli/routes.go +2 -2
| @@ -340,10 +340,10 @@ func routeTitle(route remote.RouteInfo, health map[string]*remote.RouteHealth) s | |||
| 340 | 340 | } | |
| 341 | 341 | } | |
| 342 | 342 | ||
| 343 | - | containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus)) | |
| 344 | 343 | httpIcon := components.StatusIcon(styles.IconHTTPStatus, httpHealthToStatus(health[route.Domain])) | |
| 344 | + | containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus)) | |
| 345 | 345 | ||
| 346 | - | return containerIcon + " " + httpIcon + " " + route.Domain | |
| 346 | + | return httpIcon + " " + containerIcon + " " + route.Domain | |
| 347 | 347 | } | |
| 348 | 348 | ||
| 349 | 349 | // addAttachmentChildren adds attachment nodes as children of a route node. | |
| @@ -340,10 +340,10 @@ func routeTitle(route remote.RouteInfo, health map[string]*remote.RouteHealth) s | |||||
| 340 | } | 340 | } | ||
| 341 | } | 341 | } | ||
| 342 | 342 | ||||
| 343 | - | containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus)) | |||
| 344 | httpIcon := components.StatusIcon(styles.IconHTTPStatus, httpHealthToStatus(health[route.Domain])) | 343 | httpIcon := components.StatusIcon(styles.IconHTTPStatus, httpHealthToStatus(health[route.Domain])) | ||
| 344 | + | containerIcon := components.StatusIcon(styles.IconContainerStatus, components.ParseStatus(containerStatus)) | |||
| 345 | 345 | ||||
| 346 | - | return containerIcon + " " + httpIcon + " " + route.Domain | 346 | + | return httpIcon + " " + containerIcon + " " + route.Domain |
| 347 | } | 347 | } | ||
| 348 | 348 | ||||
| 349 | // addAttachmentChildren adds attachment nodes as children of a route node. | 349 | // addAttachmentChildren adds attachment nodes as children of a route node. | ||
M internal/adapters/in/cli/ui/components/tree.go +13 -8
| @@ -70,10 +70,7 @@ func (t *Tree) Render() string { | |||
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | 72 | var b strings.Builder | |
| 73 | - | for i, item := range t.items { | |
| 74 | - | if i > 0 { | |
| 75 | - | b.WriteString("\n") | |
| 76 | - | } | |
| 73 | + | for _, item := range t.items { | |
| 77 | 74 | if item.group != nil { | |
| 78 | 75 | t.renderGroup(&b, item.group) | |
| 79 | 76 | } else { | |
| @@ -105,10 +102,17 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) { | |||
| 105 | 102 | // Title line | |
| 106 | 103 | b.WriteString(prefix + node.Title + "\n") | |
| 107 | 104 | ||
| 108 | - | // Subtitle line (muted, aligned with title text after icons) | |
| 105 | + | // Subtitle line (muted, with tree connector aligned under container icon) | |
| 109 | 106 | if node.Subtitle != "" { | |
| 107 | + | var connectorGlyph string | |
| 108 | + | if len(node.Children) > 0 { | |
| 109 | + | connectorGlyph = styles.IconTreeBranch + styles.IconTreeLine // ├─ more items below | |
| 110 | + | } else { | |
| 111 | + | connectorGlyph = styles.IconTreeLast + styles.IconTreeLine // └─ last item | |
| 112 | + | } | |
| 113 | + | connector := styles.Theme.Muted.Render(connectorGlyph) | |
| 110 | 114 | subtitle := styles.Theme.Muted.Render(node.Subtitle) | |
| 111 | - | b.WriteString(prefix + " " + subtitle + "\n") | |
| 115 | + | b.WriteString(prefix + " " + connector + " " + subtitle + "\n") | |
| 112 | 116 | } | |
| 113 | 117 | ||
| 114 | 118 | // Children | |
| @@ -128,10 +132,11 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) { | |||
| 128 | 132 | // Child title | |
| 129 | 133 | b.WriteString(prefix + " " + branch + " " + child.Title + "\n") | |
| 130 | 134 | ||
| 131 | - | // Child subtitle (indented to align under child name) | |
| 135 | + | // Child subtitle (with tree connector under child name) | |
| 132 | 136 | if child.Subtitle != "" { | |
| 137 | + | connector := styles.Theme.Muted.Render(styles.IconTreeLast + styles.IconTreeLine) | |
| 133 | 138 | subtitle := styles.Theme.Muted.Render(child.Subtitle) | |
| 134 | - | b.WriteString(prefix + " " + childPrefix + " " + subtitle + "\n") | |
| 139 | + | b.WriteString(prefix + " " + childPrefix + " " + connector + " " + subtitle + "\n") | |
| 135 | 140 | } | |
| 136 | 141 | } | |
| 137 | 142 | } | |
| @@ -70,10 +70,7 @@ func (t *Tree) Render() string { | |||||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| 72 | var b strings.Builder | 72 | var b strings.Builder | ||
| 73 | - | for i, item := range t.items { | 73 | + | for _, item := range t.items { |
| 74 | - | if i > 0 { | |||
| 75 | - | b.WriteString("\n") | |||
| 76 | - | } | |||
| 77 | if item.group != nil { | 74 | if item.group != nil { | ||
| 78 | t.renderGroup(&b, item.group) | 75 | t.renderGroup(&b, item.group) | ||
| 79 | } else { | 76 | } else { | ||
| @@ -105,10 +102,17 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) { | |||||
| 105 | // Title line | 102 | // Title line | ||
| 106 | b.WriteString(prefix + node.Title + "\n") | 103 | b.WriteString(prefix + node.Title + "\n") | ||
| 107 | 104 | ||||
| 108 | - | // Subtitle line (muted, aligned with title text after icons) | 105 | + | // Subtitle line (muted, with tree connector aligned under container icon) |
| 109 | if node.Subtitle != "" { | 106 | if node.Subtitle != "" { | ||
| 107 | + | var connectorGlyph string | |||
| 108 | + | if len(node.Children) > 0 { | |||
| 109 | + | connectorGlyph = styles.IconTreeBranch + styles.IconTreeLine // ├─ more items below | |||
| 110 | + | } else { | |||
| 111 | + | connectorGlyph = styles.IconTreeLast + styles.IconTreeLine // └─ last item | |||
| 112 | + | } | |||
| 113 | + | connector := styles.Theme.Muted.Render(connectorGlyph) | |||
| 110 | subtitle := styles.Theme.Muted.Render(node.Subtitle) | 114 | subtitle := styles.Theme.Muted.Render(node.Subtitle) | ||
| 111 | - | b.WriteString(prefix + " " + subtitle + "\n") | 115 | + | b.WriteString(prefix + " " + connector + " " + subtitle + "\n") |
| 112 | } | 116 | } | ||
| 113 | 117 | ||||
| 114 | // Children | 118 | // Children | ||
| @@ -128,10 +132,11 @@ func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) { | |||||
| 128 | // Child title | 132 | // Child title | ||
| 129 | b.WriteString(prefix + " " + branch + " " + child.Title + "\n") | 133 | b.WriteString(prefix + " " + branch + " " + child.Title + "\n") | ||
| 130 | 134 | ||||
| 131 | - | // Child subtitle (indented to align under child name) | 135 | + | // Child subtitle (with tree connector under child name) |
| 132 | if child.Subtitle != "" { | 136 | if child.Subtitle != "" { | ||
| 137 | + | connector := styles.Theme.Muted.Render(styles.IconTreeLast + styles.IconTreeLine) | |||
| 133 | subtitle := styles.Theme.Muted.Render(child.Subtitle) | 138 | subtitle := styles.Theme.Muted.Render(child.Subtitle) | ||
| 134 | - | b.WriteString(prefix + " " + childPrefix + " " + subtitle + "\n") | 139 | + | b.WriteString(prefix + " " + childPrefix + " " + connector + " " + subtitle + "\n") |
| 135 | } | 140 | } | ||
| 136 | } | 141 | } | ||
| 137 | } | 142 | } | ||