Skip to content

Commit a7f2106

Browse files
committed
add apiexport and apibinding view when viewing workspace in interactive mode
Signed-off-by: olalekan odukoya <odukoyaonline@gmail.com>
1 parent 7d1c80a commit a7f2106

File tree

2 files changed

+210
-50
lines changed

2 files changed

+210
-50
lines changed

staging/src/github.com/kcp-dev/cli/pkg/workspace/plugin/interactive.go

Lines changed: 177 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,38 @@ type treeNode struct {
3535
selectable bool
3636
children []*treeNode
3737
parent *treeNode
38+
depth int
3839
}
3940

4041
type model struct {
4142
tree *treeNode
4243
currentNode *treeNode
44+
currentIndex int
4345
selectedWorkspace *logicalcluster.Path
4446
width int
4547
height int
48+
visibleNodesCache []*treeNode
49+
cacheValid bool
50+
}
51+
52+
func (m *model) invalidateCache() {
53+
m.cacheValid = false
54+
}
55+
56+
func (m *model) getVisibleNodes() []*treeNode {
57+
if !m.cacheValid {
58+
m.visibleNodesCache = nil
59+
m.collectVisibleNodes(m.tree, &m.visibleNodesCache)
60+
m.cacheValid = true
61+
62+
for i, node := range m.visibleNodesCache {
63+
if node == m.currentNode {
64+
m.currentIndex = i
65+
break
66+
}
67+
}
68+
}
69+
return m.visibleNodesCache
4670
}
4771

4872
func (m model) Init() tea.Cmd {
@@ -72,12 +96,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7296
case "right", "l", " ":
7397
if m.currentNode != nil && !m.currentNode.expanded && len(m.currentNode.children) > 0 {
7498
m.currentNode.expanded = true
99+
m.invalidateCache()
75100
}
76101
return m, nil
77102

78103
case "left", "h", "backspace":
79104
if m.currentNode != nil && m.currentNode.expanded {
80105
m.currentNode.expanded = false
106+
m.invalidateCache()
81107
}
82108
return m, nil
83109

@@ -130,6 +156,15 @@ func (m model) View() string {
130156
if m.currentNode.info.Cluster != "" {
131157
detailParts = append(detailParts, fmt.Sprintf("cluster:%s", m.currentNode.info.Cluster))
132158
}
159+
if len(m.currentNode.info.APIExports) > 0 {
160+
detailParts = append(detailParts, fmt.Sprintf("exports:%d", len(m.currentNode.info.APIExports)))
161+
}
162+
if len(m.currentNode.info.APIExportEndpointSlices) > 0 {
163+
detailParts = append(detailParts, fmt.Sprintf("endpointslices:%d", len(m.currentNode.info.APIExportEndpointSlices)))
164+
}
165+
if len(m.currentNode.info.APIBindings) > 0 {
166+
detailParts = append(detailParts, fmt.Sprintf("bindings:%d", len(m.currentNode.info.APIBindings)))
167+
}
133168
details = detailStyle.Render(strings.Join(detailParts, " | "))
134169
}
135170

@@ -147,11 +182,127 @@ func (m model) View() string {
147182
details,
148183
)
149184

185+
visibleNodes := m.getVisibleNodes()
150186
var treeLines []string
151187
treeLines = append(treeLines, "")
152-
m.flattenTree(m.tree, 0, &treeLines)
188+
189+
for _, node := range visibleNodes {
190+
treeLines = append(treeLines, m.renderNode(node))
191+
}
192+
153193
treeLines = append(treeLines, "")
154194

195+
sectionStyle := lipgloss.NewStyle().
196+
Foreground(lipgloss.Color("245")).
197+
MarginTop(1)
198+
199+
itemStyle := lipgloss.NewStyle().
200+
Foreground(lipgloss.Color("252")).
201+
MarginLeft(2)
202+
203+
exportNameStyle := lipgloss.NewStyle().
204+
Foreground(lipgloss.Color("39"))
205+
206+
endpointSliceStyle := lipgloss.NewStyle().
207+
Foreground(lipgloss.Color("135"))
208+
209+
bindingNameStyle := lipgloss.NewStyle().
210+
Foreground(lipgloss.Color("82"))
211+
212+
bindingSourceStyle := lipgloss.NewStyle().
213+
Foreground(lipgloss.Color("248")).
214+
Italic(true)
215+
216+
boundResourceStyle := lipgloss.NewStyle().
217+
Foreground(lipgloss.Color("220"))
218+
219+
var apiDetails []string
220+
if m.currentNode != nil && m.currentNode.info != nil {
221+
var exportsColumn []string
222+
var slicesColumn []string
223+
var bindingsSection []string
224+
225+
if len(m.currentNode.info.APIExports) > 0 || len(m.currentNode.info.APIExportEndpointSlices) > 0 {
226+
availableWidth := m.width - 12
227+
columnWidth := (availableWidth - 4) / 2
228+
229+
if len(m.currentNode.info.APIExports) > 0 {
230+
exportsColumn = append(exportsColumn, sectionStyle.Render(fmt.Sprintf("APIExports (%d):", len(m.currentNode.info.APIExports))))
231+
for _, export := range m.currentNode.info.APIExports {
232+
exportsColumn = append(exportsColumn, itemStyle.Render(fmt.Sprintf("• %s", exportNameStyle.Render(export.Name))))
233+
}
234+
}
235+
236+
if len(m.currentNode.info.APIExportEndpointSlices) > 0 {
237+
slicesColumn = append(slicesColumn, sectionStyle.Render(fmt.Sprintf("APIExportEndpointSlices (%d):", len(m.currentNode.info.APIExportEndpointSlices))))
238+
for _, slice := range m.currentNode.info.APIExportEndpointSlices {
239+
sliceLine := fmt.Sprintf("• %s", endpointSliceStyle.Render(slice.Name))
240+
if slice.Spec.APIExport.Name != "" {
241+
exportPath := slice.Spec.APIExport.Path
242+
if exportPath != "" {
243+
sliceLine += fmt.Sprintf(" → %s:%s", bindingSourceStyle.Render(exportPath), slice.Spec.APIExport.Name)
244+
} else {
245+
sliceLine += fmt.Sprintf(" → %s", slice.Spec.APIExport.Name)
246+
}
247+
}
248+
if len(slice.Status.APIExportEndpoints) > 0 {
249+
sliceLine += fmt.Sprintf(" (%d endpoints)", len(slice.Status.APIExportEndpoints))
250+
}
251+
slicesColumn = append(slicesColumn, itemStyle.Render(sliceLine))
252+
}
253+
}
254+
255+
maxLines := len(exportsColumn)
256+
if len(slicesColumn) > maxLines {
257+
maxLines = len(slicesColumn)
258+
}
259+
260+
for i := 0; i < maxLines; i++ {
261+
leftLine := ""
262+
rightLine := ""
263+
if i < len(exportsColumn) {
264+
leftLine = exportsColumn[i]
265+
}
266+
if i < len(slicesColumn) {
267+
rightLine = slicesColumn[i]
268+
}
269+
leftPadded := leftLine
270+
if lipgloss.Width(leftLine) < columnWidth {
271+
leftPadded = leftLine + strings.Repeat(" ", columnWidth-lipgloss.Width(leftLine))
272+
}
273+
combinedLine := lipgloss.JoinHorizontal(lipgloss.Top, leftPadded, strings.Repeat(" ", 4), rightLine)
274+
apiDetails = append(apiDetails, combinedLine)
275+
}
276+
}
277+
278+
if len(m.currentNode.info.APIBindings) > 0 {
279+
bindingsSection = append(bindingsSection, sectionStyle.Render(fmt.Sprintf("APIBindings (%d):", len(m.currentNode.info.APIBindings))))
280+
for _, binding := range m.currentNode.info.APIBindings {
281+
bindingLine := fmt.Sprintf("• %s", bindingNameStyle.Render(binding.Name))
282+
if binding.Spec.Reference.Export != nil {
283+
exportPath := binding.Spec.Reference.Export.Path
284+
exportName := binding.Spec.Reference.Export.Name
285+
if exportPath != "" {
286+
bindingLine += fmt.Sprintf(" → %s:%s", bindingSourceStyle.Render(exportPath), exportName)
287+
} else {
288+
bindingLine += fmt.Sprintf(" → %s", bindingSourceStyle.Render(exportName))
289+
}
290+
}
291+
bindingsSection = append(bindingsSection, itemStyle.Render(bindingLine))
292+
if len(binding.Status.BoundResources) > 0 {
293+
for _, resource := range binding.Status.BoundResources {
294+
groupResource := resource.Resource
295+
if resource.Group != "" {
296+
groupResource = fmt.Sprintf("%s.%s", resource.Resource, resource.Group)
297+
}
298+
bindingsSection = append(bindingsSection, itemStyle.Render(fmt.Sprintf(" └─ %s", boundResourceStyle.Render(groupResource))))
299+
}
300+
}
301+
}
302+
apiDetails = append(apiDetails, bindingsSection...)
303+
}
304+
}
305+
155306
help := helpStyle.Render("↑/↓: navigate →/←: expand/collapse q: quit")
156307

157308
var prompt string
@@ -161,22 +312,25 @@ func (m model) View() string {
161312
Render("\nPress Enter to switch to this workspace")
162313
}
163314

164-
content := lipgloss.JoinVertical(lipgloss.Left,
165-
header,
166-
strings.Join(treeLines, "\n"),
167-
help,
168-
prompt,
169-
)
315+
var contentParts []string
316+
contentParts = append(contentParts, header)
317+
contentParts = append(contentParts, strings.Join(treeLines, "\n"))
318+
319+
if len(apiDetails) > 0 {
320+
contentParts = append(contentParts, strings.Repeat("─", m.width-8))
321+
contentParts = append(contentParts, strings.Join(apiDetails, "\n"))
322+
}
323+
324+
contentParts = append(contentParts, help)
325+
contentParts = append(contentParts, prompt)
326+
327+
content := lipgloss.JoinVertical(lipgloss.Left, contentParts...)
170328

171329
return borderStyle.Render(content)
172330
}
173331

174-
func (m *model) flattenTree(node *treeNode, depth int, lines *[]string) {
175-
if node == nil {
176-
return
177-
}
178-
179-
prefix := strings.Repeat(" ", depth)
332+
func (m *model) renderNode(node *treeNode) string {
333+
prefix := strings.Repeat(" ", node.depth)
180334

181335
icon := " "
182336
if len(node.children) > 0 {
@@ -210,44 +364,22 @@ func (m *model) flattenTree(node *treeNode, depth int, lines *[]string) {
210364
Render(" " + nodeName)
211365
}
212366

213-
*lines = append(*lines, line)
214-
215-
if node.expanded {
216-
for _, child := range node.children {
217-
m.flattenTree(child, depth+1, lines)
218-
}
219-
}
367+
return line
220368
}
221369

222370
func (m *model) moveToPrevious() {
223-
if m.currentNode == nil {
224-
return
225-
}
226-
227-
var allNodes []*treeNode
228-
m.collectVisibleNodes(m.tree, &allNodes)
229-
230-
for i, node := range allNodes {
231-
if node == m.currentNode && i > 0 {
232-
m.currentNode = allNodes[i-1]
233-
return
234-
}
371+
allNodes := m.getVisibleNodes()
372+
if m.currentIndex > 0 {
373+
m.currentIndex--
374+
m.currentNode = allNodes[m.currentIndex]
235375
}
236376
}
237377

238378
func (m *model) moveToNext() {
239-
if m.currentNode == nil {
240-
return
241-
}
242-
243-
var allNodes []*treeNode
244-
m.collectVisibleNodes(m.tree, &allNodes)
245-
246-
for i, node := range allNodes {
247-
if node == m.currentNode && i < len(allNodes)-1 {
248-
m.currentNode = allNodes[i+1]
249-
return
250-
}
379+
allNodes := m.getVisibleNodes()
380+
if m.currentIndex < len(allNodes)-1 {
381+
m.currentIndex++
382+
m.currentNode = allNodes[m.currentIndex]
251383
}
252384
}
253385

@@ -260,6 +392,7 @@ func (m *model) collectVisibleNodes(node *treeNode, nodes *[]*treeNode) {
260392

261393
if node.expanded {
262394
for _, child := range node.children {
395+
child.depth = node.depth + 1
263396
m.collectVisibleNodes(child, nodes)
264397
}
265398
}

staging/src/github.com/kcp-dev/cli/pkg/workspace/plugin/tree.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,21 @@ import (
3232
"github.com/kcp-dev/cli/pkg/base"
3333
pluginhelpers "github.com/kcp-dev/cli/pkg/helpers"
3434
"github.com/kcp-dev/logicalcluster/v3"
35+
apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
36+
apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
3537
tenancyv1alpha1 "github.com/kcp-dev/sdk/apis/tenancy/v1alpha1"
3638
kcpclientset "github.com/kcp-dev/sdk/client/clientset/versioned/cluster"
3739
)
3840

3941
// workspaceInfo contains workspace path and type information.
42+
4043
type workspaceInfo struct {
41-
Path logicalcluster.Path
42-
Type *tenancyv1alpha1.WorkspaceTypeReference
43-
Cluster string
44+
Path logicalcluster.Path
45+
Type *tenancyv1alpha1.WorkspaceTypeReference
46+
Cluster string
47+
APIExports []apisv1alpha2.APIExport
48+
APIExportEndpointSlices []apisv1alpha1.APIExportEndpointSlice
49+
APIBindings []apisv1alpha2.APIBinding
4450
}
4551

4652
// TreeOptions contains options for displaying the workspace tree.
@@ -186,10 +192,31 @@ func (o *TreeOptions) populateInteractiveNodeBubble(ctx context.Context, node *t
186192
workspaceCluster = workspace.Base()
187193
}
188194

195+
var apiExports []apisv1alpha2.APIExport
196+
exportsList, err := o.kcpClusterClient.Cluster(workspace).ApisV1alpha2().APIExports().List(ctx, metav1.ListOptions{})
197+
if err == nil {
198+
apiExports = exportsList.Items
199+
}
200+
201+
var apiExportEndpointSlices []apisv1alpha1.APIExportEndpointSlice
202+
endpointSlicesList, err := o.kcpClusterClient.Cluster(workspace).ApisV1alpha1().APIExportEndpointSlices().List(ctx, metav1.ListOptions{})
203+
if err == nil {
204+
apiExportEndpointSlices = endpointSlicesList.Items
205+
}
206+
207+
var apiBindings []apisv1alpha2.APIBinding
208+
bindingsList, err := o.kcpClusterClient.Cluster(workspace).ApisV1alpha2().APIBindings().List(ctx, metav1.ListOptions{})
209+
if err == nil {
210+
apiBindings = bindingsList.Items
211+
}
212+
189213
wsInfo := &workspaceInfo{
190-
Path: workspace,
191-
Type: workspaceType,
192-
Cluster: workspaceCluster,
214+
Path: workspace,
215+
Type: workspaceType,
216+
Cluster: workspaceCluster,
217+
APIExports: apiExports,
218+
APIExportEndpointSlices: apiExportEndpointSlices,
219+
APIBindings: apiBindings,
193220
}
194221

195222
node.info = wsInfo

0 commit comments

Comments
 (0)