Skip to content

Commit d815bb0

Browse files
authored
Show user email in render workspaces header, pre-select current workspace (#272)
Replace the workspace name with the user email in the breadcrumb header for the workspaces view only. Additionally, the current workspace is pre-selected in `render workspaces` and `render workspace set` interactive views.
1 parent bc0d56a commit d815bb0

File tree

6 files changed

+211
-6
lines changed

6 files changed

+211
-6
lines changed

cmd/workspaces.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import (
77

88
"github.com/render-oss/cli/pkg/client"
99
"github.com/render-oss/cli/pkg/command"
10+
"github.com/render-oss/cli/pkg/config"
1011
"github.com/render-oss/cli/pkg/owner"
1112
"github.com/render-oss/cli/pkg/text"
13+
"github.com/render-oss/cli/pkg/tui"
1214
"github.com/render-oss/cli/pkg/tui/views"
15+
"github.com/render-oss/cli/pkg/user"
1316
)
1417

1518
var workspacesCmd = &cobra.Command{
@@ -29,9 +32,31 @@ func loadWorkspaces(ctx context.Context) ([]*client.Owner, error) {
2932
}
3033

3134
func interactiveWorkspaces(ctx context.Context, input views.ListWorkspaceInput) {
35+
stack := tui.GetStackFromContext(ctx)
36+
stack.SetWorkspaceOverride(userEmail(ctx))
3237
command.AddToStackFunc(ctx, workspacesCmd, "Workspaces", &input, views.NewWorkspaceView(ctx, input))
3338
}
3439

40+
func userEmail(ctx context.Context) string {
41+
email, _ := config.UserEmail()
42+
if email != "" {
43+
return email
44+
}
45+
46+
c, err := client.NewDefaultClient()
47+
if err != nil {
48+
return ""
49+
}
50+
51+
u, err := user.NewRepo(c).CurrentUser(ctx)
52+
if err != nil {
53+
return ""
54+
}
55+
56+
_ = config.SetUserEmail(u.Email)
57+
return u.Email
58+
}
59+
3560
func init() {
3661
rootCmd.AddCommand(workspacesCmd)
3762

pkg/config/config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Config struct {
2626
Version int `yaml:"version"`
2727
Workspace string `yaml:"workspace"`
2828
WorkspaceName string `yaml:"workspace_name"`
29+
UserEmail string `yaml:"user_email,omitempty"`
2930
ProjectFilter string `yaml:"project_filter,omitempty"` // Project ID for filtering
3031
ProjectName string `yaml:"project_name,omitempty"` // Project name for display
3132

@@ -147,6 +148,23 @@ func WorkspaceName() (string, error) {
147148
return cfg.WorkspaceName, nil
148149
}
149150

151+
func UserEmail() (string, error) {
152+
cfg, err := Load()
153+
if err != nil {
154+
return "", err
155+
}
156+
return cfg.UserEmail, nil
157+
}
158+
159+
func SetUserEmail(email string) error {
160+
cfg, err := Load()
161+
if err != nil {
162+
return err
163+
}
164+
cfg.UserEmail = email
165+
return cfg.Persist()
166+
}
167+
150168
func GetProjectFilter() (projectID string, projectName string, err error) {
151169
cfg, err := Load()
152170
if err != nil {

pkg/tui/stack.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ type StackModel struct {
3939
loadingSpinner *spinner.Model
4040
stack []ModelWithCmd
4141

42-
width int
43-
height int
44-
done func(msg tea.Msg) (tea.Model, tea.Cmd)
45-
loadingMsg string
42+
width int
43+
height int
44+
done func(msg tea.Msg) (tea.Model, tea.Cmd)
45+
loadingMsg string
46+
workspaceOverride *string
4647
}
4748

4849
type ModelWithCmd struct {
@@ -88,6 +89,10 @@ func (m *StackModel) WithDone(f func(tea.Msg) (tea.Model, tea.Cmd)) {
8889
m.done = f
8990
}
9091

92+
func (m *StackModel) SetWorkspaceOverride(label string) {
93+
m.workspaceOverride = &label
94+
}
95+
9196
func newSpinner() *spinner.Model {
9297
spin := spinner.New()
9398
spin.Spinner = spinner.Dot
@@ -257,7 +262,12 @@ func (m *StackModel) View() string {
257262
func (m *StackModel) header() string {
258263
var breadCrumbs []string
259264

260-
workspace, _ := config.WorkspaceName()
265+
var workspace string
266+
if m.workspaceOverride != nil {
267+
workspace = *m.workspaceOverride
268+
} else {
269+
workspace, _ = config.WorkspaceName()
270+
}
261271
if workspace != "" {
262272
breadCrumbs = append(breadCrumbs, stackInfoStyle.Render(workspace))
263273
}

pkg/tui/stack_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"os"
78
"testing"
89
"time"
910

@@ -157,3 +158,74 @@ func TestStack(t *testing.T) {
157158
require.NoError(t, err)
158159
})
159160
}
161+
162+
func setupTestConfig(t *testing.T, content string) {
163+
t.Helper()
164+
f, err := os.CreateTemp("", "render-config")
165+
require.NoError(t, err)
166+
t.Cleanup(func() { _ = os.Remove(f.Name()) })
167+
168+
_, err = f.WriteString(content)
169+
require.NoError(t, err)
170+
require.NoError(t, f.Close())
171+
172+
t.Setenv("RENDER_CLI_CONFIG_PATH", f.Name())
173+
}
174+
175+
func TestStackHeader(t *testing.T) {
176+
t.Run("shows workspace name from config when no override set", func(t *testing.T) {
177+
setupTestConfig(t, "version: 1\nworkspace: tea-123\nworkspace_name: MyTeam\n")
178+
179+
stack := tui.NewStack()
180+
stack.Push(tui.ModelWithCmd{Model: &testhelper.SimpleModel{Str: "content"}, Breadcrumb: "Services"})
181+
tm := teatest.NewTestModel(t, stack)
182+
183+
tm.Send(tea.WindowSizeMsg{Width: 80, Height: 24})
184+
185+
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
186+
return bytes.Contains(bts, []byte("MyTeam")) && bytes.Contains(bts, []byte("Services"))
187+
}, teatest.WithCheckInterval(time.Millisecond*1), teatest.WithDuration(time.Second*3))
188+
189+
err := tm.Quit()
190+
require.NoError(t, err)
191+
})
192+
193+
t.Run("shows workspace override instead of config workspace name", func(t *testing.T) {
194+
setupTestConfig(t, "version: 1\nworkspace: tea-123\nworkspace_name: MyTeam\n")
195+
196+
stack := tui.NewStack()
197+
stack.SetWorkspaceOverride("user@example.com")
198+
stack.Push(tui.ModelWithCmd{Model: &testhelper.SimpleModel{Str: "content"}, Breadcrumb: "Workspaces"})
199+
tm := teatest.NewTestModel(t, stack)
200+
201+
tm.Send(tea.WindowSizeMsg{Width: 80, Height: 24})
202+
203+
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
204+
return bytes.Contains(bts, []byte("user@example.com")) &&
205+
bytes.Contains(bts, []byte("Workspaces")) &&
206+
!bytes.Contains(bts, []byte("MyTeam"))
207+
}, teatest.WithCheckInterval(time.Millisecond*1), teatest.WithDuration(time.Second*3))
208+
209+
err := tm.Quit()
210+
require.NoError(t, err)
211+
})
212+
213+
t.Run("shows only breadcrumb when override is empty string", func(t *testing.T) {
214+
setupTestConfig(t, "version: 1\nworkspace: tea-123\nworkspace_name: MyTeam\n")
215+
216+
stack := tui.NewStack()
217+
stack.SetWorkspaceOverride("")
218+
stack.Push(tui.ModelWithCmd{Model: &testhelper.SimpleModel{Str: "content"}, Breadcrumb: "Workspaces"})
219+
tm := teatest.NewTestModel(t, stack)
220+
221+
tm.Send(tea.WindowSizeMsg{Width: 80, Height: 24})
222+
223+
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
224+
return bytes.Contains(bts, []byte("Workspaces")) &&
225+
!bytes.Contains(bts, []byte("MyTeam"))
226+
}, teatest.WithCheckInterval(time.Millisecond*1), teatest.WithDuration(time.Second*3))
227+
228+
err := tm.Quit()
229+
require.NoError(t, err)
230+
})
231+
}

pkg/tui/views/workspacelist.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,19 @@ func (v *WorkspaceView) Init() tea.Cmd {
159159
}
160160

161161
func (v *WorkspaceView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
162-
return v.table.Update(msg)
162+
model, cmd := v.table.Update(msg)
163+
164+
if _, ok := msg.(tui.LoadDataMsg[[]*client.Owner]); ok {
165+
currentID, _ := config.WorkspaceID()
166+
for i, row := range v.table.Model.GetVisibleRows() {
167+
if id, _ := row.Data["ID"].(string); id == currentID {
168+
v.table.Model = v.table.Model.WithHighlightedRow(i)
169+
break
170+
}
171+
}
172+
}
173+
174+
return model, cmd
163175
}
164176

165177
func (v *WorkspaceView) View() string {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package views_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/render-oss/cli/pkg/client"
12+
"github.com/render-oss/cli/pkg/tui"
13+
"github.com/render-oss/cli/pkg/tui/views"
14+
)
15+
16+
func setupTestConfig(t *testing.T, content string) {
17+
t.Helper()
18+
f, err := os.CreateTemp("", "render-config")
19+
require.NoError(t, err)
20+
t.Cleanup(func() { _ = os.Remove(f.Name()) })
21+
22+
_, err = f.WriteString(content)
23+
require.NoError(t, err)
24+
require.NoError(t, f.Close())
25+
26+
t.Setenv("RENDER_CLI_CONFIG_PATH", f.Name())
27+
}
28+
29+
func TestWorkspaceViewHighlightsCurrentWorkspace(t *testing.T) {
30+
owners := []*client.Owner{
31+
{Id: "tea-111", Name: "Team1", Email: "team1@test.com", Type: "team"},
32+
{Id: "tea-222", Name: "Team2", Email: "team2@test.com", Type: "team"},
33+
{Id: "tea-333", Name: "Team3", Email: "team3@test.com", Type: "team"},
34+
}
35+
36+
t.Run("highlights the current workspace", func(t *testing.T) {
37+
setupTestConfig(t, "version: 1\nworkspace: tea-222\nworkspace_name: Team2\n")
38+
39+
view := views.NewWorkspaceView(context.Background(), views.ListWorkspaceInput{})
40+
model, _ := view.Update(tui.LoadDataMsg[[]*client.Owner]{Data: owners})
41+
42+
table, ok := model.(*tui.Table[*client.Owner])
43+
require.True(t, ok)
44+
assert.Equal(t, 1, table.Model.GetHighlightedRowIndex())
45+
})
46+
47+
t.Run("highlights first row when no workspace is set", func(t *testing.T) {
48+
setupTestConfig(t, "version: 1\n")
49+
50+
view := views.NewWorkspaceView(context.Background(), views.ListWorkspaceInput{})
51+
model, _ := view.Update(tui.LoadDataMsg[[]*client.Owner]{Data: owners})
52+
53+
table, ok := model.(*tui.Table[*client.Owner])
54+
require.True(t, ok)
55+
assert.Equal(t, 0, table.Model.GetHighlightedRowIndex())
56+
})
57+
58+
t.Run("highlights first row when workspace ID has no match", func(t *testing.T) {
59+
setupTestConfig(t, "version: 1\nworkspace: tea-999\nworkspace_name: Missing\n")
60+
61+
view := views.NewWorkspaceView(context.Background(), views.ListWorkspaceInput{})
62+
model, _ := view.Update(tui.LoadDataMsg[[]*client.Owner]{Data: owners})
63+
64+
table, ok := model.(*tui.Table[*client.Owner])
65+
require.True(t, ok)
66+
assert.Equal(t, 0, table.Model.GetHighlightedRowIndex())
67+
})
68+
}

0 commit comments

Comments
 (0)