Skip to content

Commit a6f3dc4

Browse files
authored
Add render workspaces command (#255)
Adds a new subcommand to list all accessible workspaces with interactive (default) and non-interactive output modes. # Interactive mode (default) render workspaces # Non-interactive outputs render workspaces -o text # Plain text table render workspaces -o json # JSON array render workspaces -o yaml # YAML output
1 parent b3d4f21 commit a6f3dc4

4 files changed

Lines changed: 266 additions & 0 deletions

File tree

cmd/workspaces.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
6+
"github.com/spf13/cobra"
7+
8+
"github.com/render-oss/cli/pkg/client"
9+
"github.com/render-oss/cli/pkg/command"
10+
"github.com/render-oss/cli/pkg/owner"
11+
"github.com/render-oss/cli/pkg/text"
12+
"github.com/render-oss/cli/pkg/tui/views"
13+
)
14+
15+
var workspacesCmd = &cobra.Command{
16+
Use: "workspaces",
17+
Short: "List workspaces",
18+
GroupID: GroupCore.ID,
19+
}
20+
21+
func loadWorkspaces(ctx context.Context) ([]*client.Owner, error) {
22+
c, err := client.NewDefaultClient()
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
ownerRepo := owner.NewRepo(c)
28+
return ownerRepo.ListOwners(ctx, owner.ListInput{})
29+
}
30+
31+
func interactiveWorkspaces(ctx context.Context, input views.ListWorkspaceInput) {
32+
command.AddToStackFunc(ctx, workspacesCmd, "Workspaces", &input, views.NewWorkspaceView(ctx, input))
33+
}
34+
35+
func init() {
36+
rootCmd.AddCommand(workspacesCmd)
37+
38+
workspacesCmd.RunE = func(cmd *cobra.Command, args []string) error {
39+
var input views.ListWorkspaceInput
40+
err := command.ParseCommand(cmd, args, &input)
41+
if err != nil {
42+
return err
43+
}
44+
45+
if nonInteractive, err := command.NonInteractive(cmd, func() ([]*client.Owner, error) {
46+
return loadWorkspaces(cmd.Context())
47+
}, text.WorkspaceTable); err != nil {
48+
return err
49+
} else if nonInteractive {
50+
return nil
51+
}
52+
53+
interactiveWorkspaces(cmd.Context(), input)
54+
return nil
55+
}
56+
}

pkg/owner/repo_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package owner_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/render-oss/cli/pkg/client"
14+
"github.com/render-oss/cli/pkg/owner"
15+
)
16+
17+
func TestRepo_ListOwners(t *testing.T) {
18+
t.Run("returns list of owners", func(t *testing.T) {
19+
owners := []client.OwnerWithCursor{
20+
{Owner: &client.Owner{Id: "tea-abc123", Name: "Team Alpha", Email: "alpha@example.com"}},
21+
{Owner: &client.Owner{Id: "usr-def456", Name: "User Beta", Email: "beta@example.com"}},
22+
}
23+
24+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25+
assert.Equal(t, "/owners", r.URL.Path)
26+
assert.Equal(t, "100", r.URL.Query().Get("limit"))
27+
28+
w.Header().Set("Content-Type", "application/json")
29+
json.NewEncoder(w).Encode(owners)
30+
}))
31+
defer server.Close()
32+
33+
c, err := client.NewClientWithResponses(server.URL)
34+
require.NoError(t, err)
35+
36+
repo := owner.NewRepo(c)
37+
result, err := repo.ListOwners(context.Background(), owner.ListInput{})
38+
39+
require.NoError(t, err)
40+
require.Len(t, result, 2)
41+
assert.Equal(t, "tea-abc123", result[0].Id)
42+
assert.Equal(t, "Team Alpha", result[0].Name)
43+
assert.Equal(t, "usr-def456", result[1].Id)
44+
assert.Equal(t, "User Beta", result[1].Name)
45+
})
46+
47+
t.Run("filters by name", func(t *testing.T) {
48+
owners := []client.OwnerWithCursor{
49+
{Owner: &client.Owner{Id: "tea-abc123", Name: "Team Alpha", Email: "alpha@example.com"}},
50+
}
51+
52+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53+
assert.Equal(t, "/owners", r.URL.Path)
54+
assert.Contains(t, r.URL.Query()["name"], "Team Alpha")
55+
56+
w.Header().Set("Content-Type", "application/json")
57+
json.NewEncoder(w).Encode(owners)
58+
}))
59+
defer server.Close()
60+
61+
c, err := client.NewClientWithResponses(server.URL)
62+
require.NoError(t, err)
63+
64+
repo := owner.NewRepo(c)
65+
result, err := repo.ListOwners(context.Background(), owner.ListInput{Name: "Team Alpha"})
66+
67+
require.NoError(t, err)
68+
require.Len(t, result, 1)
69+
assert.Equal(t, "Team Alpha", result[0].Name)
70+
})
71+
72+
t.Run("returns empty list when no owners", func(t *testing.T) {
73+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74+
w.Header().Set("Content-Type", "application/json")
75+
json.NewEncoder(w).Encode([]client.OwnerWithCursor{})
76+
}))
77+
defer server.Close()
78+
79+
c, err := client.NewClientWithResponses(server.URL)
80+
require.NoError(t, err)
81+
82+
repo := owner.NewRepo(c)
83+
result, err := repo.ListOwners(context.Background(), owner.ListInput{})
84+
85+
require.NoError(t, err)
86+
assert.Empty(t, result)
87+
})
88+
}
89+
90+
func TestRepo_RetrieveOwner(t *testing.T) {
91+
t.Run("returns owner by id", func(t *testing.T) {
92+
expectedOwner := &client.Owner{
93+
Id: "tea-abc123",
94+
Name: "Team Alpha",
95+
Email: "alpha@example.com",
96+
}
97+
98+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99+
assert.Equal(t, "/owners/tea-abc123", r.URL.Path)
100+
101+
w.Header().Set("Content-Type", "application/json")
102+
json.NewEncoder(w).Encode(expectedOwner)
103+
}))
104+
defer server.Close()
105+
106+
c, err := client.NewClientWithResponses(server.URL)
107+
require.NoError(t, err)
108+
109+
repo := owner.NewRepo(c)
110+
result, err := repo.RetrieveOwner(context.Background(), "tea-abc123")
111+
112+
require.NoError(t, err)
113+
assert.Equal(t, "tea-abc123", result.Id)
114+
assert.Equal(t, "Team Alpha", result.Name)
115+
assert.Equal(t, "alpha@example.com", result.Email)
116+
})
117+
118+
t.Run("returns error when owner not found", func(t *testing.T) {
119+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120+
w.WriteHeader(http.StatusNotFound)
121+
}))
122+
defer server.Close()
123+
124+
c, err := client.NewClientWithResponses(server.URL)
125+
require.NoError(t, err)
126+
127+
repo := owner.NewRepo(c)
128+
_, err = repo.RetrieveOwner(context.Background(), "nonexistent")
129+
130+
require.Error(t, err)
131+
})
132+
}

pkg/text/table.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ func TaskRunTable(v []*wfclient.TaskRun) string {
9797
return FormatString(t.Render())
9898
}
9999

100+
func WorkspaceTable(v []*client.Owner) string {
101+
t := newTable()
102+
t.AppendHeader(table.Row{"Name", "Email", "ID"})
103+
for _, o := range v {
104+
t.AppendRow(table.Row{o.Name, o.Email, o.Id})
105+
}
106+
return FormatString(t.Render())
107+
}
108+
100109
func newTable() table.Writer {
101110
t := table.NewWriter()
102111
t.Style().Options.DrawBorder = false

pkg/text/table_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package text_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/render-oss/cli/pkg/client"
10+
"github.com/render-oss/cli/pkg/text"
11+
)
12+
13+
func TestWorkspaceTable(t *testing.T) {
14+
t.Run("formats workspaces correctly", func(t *testing.T) {
15+
workspaces := []*client.Owner{
16+
{
17+
Name: "My Workspace",
18+
Email: "user@example.com",
19+
Id: "tea-abc123",
20+
},
21+
{
22+
Name: "Team Workspace",
23+
Email: "team@example.com",
24+
Id: "tea-def456",
25+
},
26+
}
27+
28+
result := text.WorkspaceTable(workspaces)
29+
30+
assert.Contains(t, result, "NAME")
31+
assert.Contains(t, result, "EMAIL")
32+
assert.Contains(t, result, "ID")
33+
assert.Contains(t, result, "My Workspace")
34+
assert.Contains(t, result, "user@example.com")
35+
assert.Contains(t, result, "tea-abc123")
36+
assert.Contains(t, result, "Team Workspace")
37+
assert.Contains(t, result, "team@example.com")
38+
assert.Contains(t, result, "tea-def456")
39+
})
40+
41+
t.Run("handles empty list", func(t *testing.T) {
42+
workspaces := []*client.Owner{}
43+
44+
result := text.WorkspaceTable(workspaces)
45+
46+
assert.Contains(t, result, "NAME")
47+
assert.Contains(t, result, "EMAIL")
48+
assert.Contains(t, result, "ID")
49+
// Should only have header, no data rows
50+
lines := strings.Split(strings.TrimSpace(result), "\n")
51+
assert.Equal(t, 1, len(lines))
52+
})
53+
54+
t.Run("handles single workspace", func(t *testing.T) {
55+
workspaces := []*client.Owner{
56+
{
57+
Name: "Solo Workspace",
58+
Email: "solo@example.com",
59+
Id: "usr-solo123",
60+
},
61+
}
62+
63+
result := text.WorkspaceTable(workspaces)
64+
65+
assert.Contains(t, result, "Solo Workspace")
66+
assert.Contains(t, result, "solo@example.com")
67+
assert.Contains(t, result, "usr-solo123")
68+
})
69+
}

0 commit comments

Comments
 (0)