Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ checks:
| Inspector Mode | `i` open mode from the service list, `Enter` open the selected workflow, `l` open the checklist file picker |
| Security Inspector | `r` run/rescan, `1`-`5` severity filter, `Enter` finding detail |
| Checklist Inspector | `l` load or switch checklist files, `r` run/rerun the loaded checklist, `Enter` result detail |
| Context Picker | `a` add context, type or `/` filter, `s` setup selected context and quit, `y` copy selected exports and quit, `u` clear shell context and quit with a final confirmation message |
| Context Picker | `a` add context, type or `/` filter, `s` setup selected context and quit, `y` copy selected exports and quit, filter-mode `Ctrl+S` setup selected filtered context, filter-mode `Ctrl+Y` copy selected filtered exports, `u` clear shell context and quit with a final confirmation message |
| ECR | `Enter` images, `d` repository detail, `/` filter, `r` refresh, image detail `c` copy digest, `t` copy tag |
| Lambda | `Enter` invoke, `d` detail, `l` view CloudWatch Logs, `/` filter, `r` refresh |

Expand Down
6 changes: 6 additions & 0 deletions internal/app/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ func (m Model) helpModeShortcuts() []helpShortcut {
{"backspace", "Delete the previous character"},
{"↑/↓", "Move through filtered results"},
}
if m.screen == screenContextPicker {
shortcuts = append(shortcuts,
helpShortcut{"ctrl+y", "Copy shell exports for the selected filtered context and quit"},
helpShortcut{"ctrl+s", "Set up the selected filtered context for the shell and quit"},
)
}
if m.screen == screenCWLogViewer {
shortcuts = append(shortcuts, helpShortcut{"enter", "Apply the log filter and reload events"})
} else {
Expand Down
21 changes: 21 additions & 0 deletions internal/app/screen_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}

if m.isFiltering(filterContexts) {
switch key {
case "ctrl+s":
selected, ok := m.selectedContextInfo()
if !ok {
return m, nil
}
return m.beginContextSetup(selected)
case "ctrl+y":
selected, ok := m.selectedContextInfo()
if !ok {
return m, nil
}
return m.beginContextExport(selected)
}
}

if cmd, handled := m.updateSharedFilter(msg, filterContexts); handled {
m.syncContextTable()
return m, cmd
Expand Down Expand Up @@ -275,6 +292,10 @@ func (m Model) viewContextPicker() string {

b.WriteString(m.renderListPanel(panel.String()))
b.WriteString("\n\n")
if m.isFiltering(filterContexts) {
b.WriteString(m.renderHelpBar("filtering: type search • ↑/↓ choose • enter finish • esc clear • ctrl+y copy env • ctrl+s setup"))
return b.String()
}
if m.cfg.ContextName != "" {
b.WriteString(m.renderHelpBar("↑/↓: navigate • type: filter • /: filter • enter: switch • s: setup • y: copy env • u: unset • a: add • esc: clear/back • q: quit"))
} else {
Expand Down
201 changes: 201 additions & 0 deletions internal/app/screen_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,51 @@ func TestContextPickerFilterUpdatesTableRows(t *testing.T) {
}
}

func TestContextPickerFilterModePlainYAndSAppendToFilter(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
m.height = 20

updated, _ := m.Update(contextsLoadedMsg{contexts: testContexts()})
model := updated.(Model)

updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
model = updated.(Model)
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'y'}})
model = updated.(Model)
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}})
model = updated.(Model)

if !model.isFiltering(filterContexts) {
t.Fatal("expected context filter to remain active")
}
if got := model.filterValue(filterContexts); got != "ys" {
t.Fatalf("expected plain y/s to append to filter, got %q", got)
}
if model.screen != screenContextPicker {
t.Fatalf("expected to remain on context picker, got %v", model.screen)
}
}

func TestContextPickerFilterModeHelpShowsModifierActions(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
m.height = 20

updated, _ := m.Update(contextsLoadedMsg{contexts: testContexts()})
model := updated.(Model)

updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
model = updated.(Model)

view := model.viewContextPicker()
for _, want := range []string{"filtering: type search", "ctrl+y copy", "env", "ctrl+s setup"} {
if !strings.Contains(view, want) {
t.Fatalf("expected filter-mode help to contain %q, got %q", want, view)
}
}
}

func TestContextPickerIncrementalFilterStartsOnTyping(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
Expand Down Expand Up @@ -410,6 +455,162 @@ contexts:
}
}

func TestContextPickerFilterModeCtrlYCopiesSelectedFilteredContext(t *testing.T) {
path := writeContextConfig(t, `
current: prod
defaults:
region: us-east-1
contexts:
- name: dev
auth_type: credential
profile: dev-profile
region: us-east-1
- name: prod
auth_type: credential
profile: prod-profile
region: us-west-2
`)

origBuildEnv := contextBuildEnvExportsFn
origCopy := contextCopyClipboardFn
defer func() {
contextBuildEnvExportsFn = origBuildEnv
contextCopyClipboardFn = origCopy
}()

var copied string
contextBuildEnvExportsFn = func(_ context.Context, cfg *config.Config) (string, error) {
return "exports-for-" + cfg.ContextName, nil
}
contextCopyClipboardFn = func(text string) error {
copied = text
return nil
}

m := New(testConfig(), path, "dev")
m.width = 80
m.height = 20

updated, _ := m.Update(contextsLoadedMsg{contexts: []config.ContextInfo{
{Name: "dev", Region: "us-east-1", AuthType: "credential"},
{Name: "prod", Region: "us-west-2", AuthType: "credential", Current: true},
}})
model := updated.(Model)

updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
model = updated.(Model)
for _, ch := range []rune("dev") {
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
model = updated.(Model)
}
updated, cmd := model.Update(tea.KeyMsg{Type: tea.KeyCtrlY})
model = updated.(Model)
if model.screen != screenLoading || cmd == nil {
t.Fatalf("expected loading command for filter-mode ctrl+y, got screen=%v cmd=%v", model.screen, cmd)
}

for _, batchedCmd := range cmd().(tea.BatchMsg) {
msg := batchedCmd()
if msg == nil {
continue
}
updated, _ = model.Update(msg)
model = updated.(Model)
}
if model.screen != screenExitNotice {
t.Fatalf("expected copy exports to open exit notice, got %v", model.screen)
}
if copied != "exports-for-dev" {
t.Fatalf("unexpected clipboard exports: %q", copied)
}

cfg, err := config.Load(nil, nil, path)
if err != nil {
t.Fatal(err)
}
if cfg.ContextName != "prod" {
t.Fatalf("expected current context to remain prod, got %q", cfg.ContextName)
}
}

func TestContextPickerFilterModeCtrlSSetupsSelectedFilteredContext(t *testing.T) {
path := writeContextConfig(t, `
current: prod
defaults:
region: us-east-1
contexts:
- name: dev
auth_type: credential
profile: dev-profile
region: us-east-1
- name: prod
auth_type: credential
profile: prod-profile
region: us-west-2
`)

origBuildEnv := contextBuildEnvExportsFn
origCopy := contextCopyClipboardFn
defer func() {
contextBuildEnvExportsFn = origBuildEnv
contextCopyClipboardFn = origCopy
}()

var copied string
contextBuildEnvExportsFn = func(_ context.Context, cfg *config.Config) (string, error) {
return fmt.Sprintf("export %s='%s'", auth.ContextEnvVar, cfg.ContextName), nil
}
contextCopyClipboardFn = func(text string) error {
copied = text
return nil
}

m := New(testConfig(), path, "dev")
m.width = 80
m.height = 20

updated, _ := m.Update(contextsLoadedMsg{contexts: []config.ContextInfo{
{Name: "dev", Region: "us-east-1", AuthType: "credential"},
{Name: "prod", Region: "us-west-2", AuthType: "credential", Current: true},
}})
model := updated.(Model)

updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
model = updated.(Model)
for _, ch := range []rune("dev") {
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
model = updated.(Model)
}
updated, cmd := model.Update(tea.KeyMsg{Type: tea.KeyCtrlS})
model = updated.(Model)
if model.screen != screenLoading || cmd == nil {
t.Fatalf("expected loading command for filter-mode ctrl+s, got screen=%v cmd=%v", model.screen, cmd)
}

for _, batchedCmd := range cmd().(tea.BatchMsg) {
msg := batchedCmd()
if msg == nil {
continue
}
updated, _ = model.Update(msg)
model = updated.(Model)
}
if model.screen != screenExitNotice {
t.Fatalf("expected setup to open exit notice, got %v", model.screen)
}
if copied != "export UNIC_CONTEXT='dev'" {
t.Fatalf("unexpected clipboard exports: %q", copied)
}

cfg, err := config.Load(nil, nil, path)
if err != nil {
t.Fatal(err)
}
if cfg.ContextName != "dev" {
t.Fatalf("expected current context to switch to dev, got %q", cfg.ContextName)
}
}

func TestSwitchContextSSOReusesCachedSessionWithoutLoginCommand(t *testing.T) {
path := writeContextConfig(t, `
current: dev-sso
Expand Down
Loading