feat: a Fields pane for anything but a secret, first for Azure accounts: fields and groups to move through, filter and copy #7
No files matched your search
+2
-2
@@ -20,8 +20,8 @@ and the token lazysecrets already has. Below them are the groups the
|
|||||||
account is in, nested ones too: those come from Microsoft Graph, once a
|
account is in, nested ones too: those come from Microsoft Graph, once a
|
||||||
session, the first time the pane shows them. `y` on the row copies the
|
session, the first time the pane shows them. `y` on the row copies the
|
||||||
object ID, the one access policies and role assignments ask for; `tab`
|
object ID, the one access policies and role assignments ask for; `tab`
|
||||||
goes into the pane, where `j`/`k` pick any field or group and `y` copies
|
goes into the pane, where `j`/`k` pick any field or group, `/` filters
|
||||||
it.
|
them (`/tenant`, `/platform`) and `y` copies the selected one.
|
||||||
|
|
||||||
If an account's az login has expired, the status line says so once, and the
|
If an account's az login has expired, the status line says so once, and the
|
||||||
Details pane on that account's rows says it until you sign in: press `L`
|
Details pane on that account's rows says it until you sign in: press `L`
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ func New(providers []provider.Provider, opts Options) Model {
|
|||||||
nvimOpen: nvimOpen,
|
nvimOpen: nvimOpen,
|
||||||
cache: newCache(),
|
cache: newCache(),
|
||||||
groups: map[string]groupsState{},
|
groups: map[string]groupsState{},
|
||||||
|
srcDetail: panes.NewSourceDetail(),
|
||||||
}
|
}
|
||||||
if m.clip == nil {
|
if m.clip == nil {
|
||||||
m.clip = clipboard.New()
|
m.clip = clipboard.New()
|
||||||
@@ -927,6 +928,10 @@ func (m *Model) focusedFilter() filterable {
|
|||||||
return &m.containers
|
return &m.containers
|
||||||
case paneSecrets:
|
case paneSecrets:
|
||||||
return &m.secrets
|
return &m.secrets
|
||||||
|
case paneDetail:
|
||||||
|
if m.srcDetail.Shown() {
|
||||||
|
return &m.srcDetail
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1115,7 +1115,7 @@ func TestAccountPane(t *testing.T) {
|
|||||||
t.Fatalf("focus %v, want the fourth pane", d.m.focus)
|
t.Fatalf("focus %v, want the fourth pane", d.m.focus)
|
||||||
}
|
}
|
||||||
scr := d.screen()
|
scr := d.screen()
|
||||||
for _, want := range []string{"Account", "Alice Example", "groups 2", "Platform team", "Vault readers"} {
|
for _, want := range []string{"Alice Example", "groups 2", "Platform team", "Vault readers"} {
|
||||||
if !strings.Contains(scr, want) {
|
if !strings.Contains(scr, want) {
|
||||||
t.Errorf("missing %q:\n%s", want, scr)
|
t.Errorf("missing %q:\n%s", want, scr)
|
||||||
}
|
}
|
||||||
@@ -1132,6 +1132,23 @@ func TestAccountPane(t *testing.T) {
|
|||||||
if got := d.clip().content; got != "Vault readers" {
|
if got := d.clip().content; got != "Vault readers" {
|
||||||
t.Errorf("y on a group copied %q", got)
|
t.Errorf("y on a group copied %q", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// / filters it, labels too.
|
||||||
|
d.keys("/")
|
||||||
|
d.typeText("vault")
|
||||||
|
d.keys("enter")
|
||||||
|
if scr := d.screen(); !strings.Contains(scr, "Vault readers") || strings.Contains(scr, "Platform team") {
|
||||||
|
t.Errorf("filtered:\n%s", scr)
|
||||||
|
}
|
||||||
|
d.keys("y")
|
||||||
|
if got := d.clip().content; got != "Vault readers" {
|
||||||
|
t.Errorf("y in the filtered pane copied %q", got)
|
||||||
|
}
|
||||||
|
d.keys("esc")
|
||||||
|
if !strings.Contains(d.screen(), "Platform team") {
|
||||||
|
t.Errorf("filter not cleared:\n%s", d.screen())
|
||||||
|
}
|
||||||
|
|
||||||
d.keys("shift+tab")
|
d.keys("shift+tab")
|
||||||
if d.m.focus != paneSources {
|
if d.m.focus != paneSources {
|
||||||
t.Errorf("back to %v, want Sources", d.m.focus)
|
t.Errorf("back to %v, want Sources", d.m.focus)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (c contextHelp) ShortHelp() []key.Binding {
|
|||||||
}
|
}
|
||||||
if c.focus == paneDetail && c.account {
|
if c.focus == paneDetail && c.account {
|
||||||
copyField := key.NewBinding(key.WithKeys(k.Copy.Keys()...), key.WithHelp(k.Copy.Help().Key, "copy"))
|
copyField := key.NewBinding(key.WithKeys(k.Copy.Keys()...), key.WithHelp(k.Copy.Help().Key, "copy"))
|
||||||
return append(b, k.Up, k.Down, copyField, k.Help, k.Quit)
|
return append(b, k.Up, k.Down, copyField, k.Filter, k.Help, k.Quit)
|
||||||
}
|
}
|
||||||
switch c.focus {
|
switch c.focus {
|
||||||
case paneSources:
|
case paneSources:
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func (m Model) panesView() string {
|
|||||||
func() string { return m.secrets.View(m.st, m.active(paneSecrets), spin) },
|
func() string { return m.secrets.View(m.st, m.active(paneSecrets), spin) },
|
||||||
func() string {
|
func() string {
|
||||||
if m.srcDetail.Shown() {
|
if m.srcDetail.Shown() {
|
||||||
return m.srcDetail.View(m.st, m.active(paneDetail))
|
return m.srcDetail.View(m.st, m.active(paneDetail), spin)
|
||||||
}
|
}
|
||||||
return m.detail.View(m.st, m.active(paneDetail), spin)
|
return m.detail.View(m.st, m.active(paneDetail), spin)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func (m *Model) syncSourceDetail() tea.Cmd {
|
|||||||
}
|
}
|
||||||
fields = append(fields, groupFields(st)...)
|
fields = append(fields, groupFields(st)...)
|
||||||
}
|
}
|
||||||
m.srcDetail.Set("Account", fields)
|
m.srcDetail.Set(m.byID[r.provider].Label(), fields)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package panes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
@@ -10,12 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// SourceDetail is the fourth pane on a provider's row: what's known about
|
// SourceDetail is the fourth pane on a provider's row: what's known about
|
||||||
// its account, one field per row, to move through and copy from.
|
// its account, one field per row, to move through, filter and copy from.
|
||||||
|
// It's a List, so / filters it like the other panes.
|
||||||
type SourceDetail struct {
|
type SourceDetail struct {
|
||||||
title string
|
List[Field]
|
||||||
fields []Field
|
|
||||||
sel int
|
|
||||||
w, h int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field is a row: a value under a label. Info rows (loading, errors)
|
// Field is a row: a value under a label. Info rows (loading, errors)
|
||||||
@@ -25,64 +22,44 @@ type Field struct {
|
|||||||
Info bool
|
Info bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set shows fields under title, keeping the cursor where it was for the
|
func NewSourceDetail() SourceDetail {
|
||||||
// same title.
|
l := NewList("", Item[Field]{
|
||||||
|
ID: func(f Field) string { return f.Label + "\x00" + f.Value },
|
||||||
|
Filter: func(f Field) string { return f.Value },
|
||||||
|
// "/tenant" finds the tenant's rows.
|
||||||
|
FilterExtra: func(f Field) string { return f.Label },
|
||||||
|
Render: func(st styles.Styles, f Field, width int) string {
|
||||||
|
value := f.Value
|
||||||
|
if f.Info {
|
||||||
|
value = st.Muted.Render(value)
|
||||||
|
}
|
||||||
|
return ansi.Truncate(st.Label.Render(fmt.Sprintf("%-10s", f.Label))+" "+value, width, "…")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
l.Empty = "nothing known about this account"
|
||||||
|
return SourceDetail{l}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set shows fields under title. Another title (another account) starts
|
||||||
|
// over, without a filter.
|
||||||
func (s *SourceDetail) Set(title string, fields []Field) {
|
func (s *SourceDetail) Set(title string, fields []Field) {
|
||||||
if title != s.title {
|
if title != s.Title {
|
||||||
s.sel = 0
|
s.Reset()
|
||||||
|
s.Title = title
|
||||||
|
}
|
||||||
|
if len(fields) > 0 || len(s.Items()) > 0 {
|
||||||
|
s.SetItems(fields)
|
||||||
}
|
}
|
||||||
s.title, s.fields = title, fields
|
|
||||||
s.sel = max(0, min(s.sel, len(fields)-1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shown reports whether there's anything to show.
|
// Shown reports whether there's anything to show.
|
||||||
func (s SourceDetail) Shown() bool { return len(s.fields) > 0 }
|
func (s SourceDetail) Shown() bool { return len(s.Items()) > 0 }
|
||||||
|
|
||||||
func (s *SourceDetail) SetSize(w, h int) { s.w, s.h = w, h }
|
|
||||||
|
|
||||||
func (s *SourceDetail) Move(delta int) { s.sel = max(0, min(s.sel+delta, len(s.fields)-1)) }
|
|
||||||
func (s *SourceDetail) Top() { s.sel = 0 }
|
|
||||||
func (s *SourceDetail) Bottom() { s.sel = max(0, len(s.fields)-1) }
|
|
||||||
|
|
||||||
// Selected is the field under the cursor, unless it's an info row.
|
// Selected is the field under the cursor, unless it's an info row.
|
||||||
func (s SourceDetail) Selected() (Field, bool) {
|
func (s SourceDetail) Selected() (Field, bool) {
|
||||||
if s.sel >= len(s.fields) || s.fields[s.sel].Info {
|
f, ok := s.List.Selected()
|
||||||
|
if !ok || f.Info {
|
||||||
return Field{}, false
|
return Field{}, false
|
||||||
}
|
}
|
||||||
return s.fields[s.sel], true
|
return f, true
|
||||||
}
|
|
||||||
|
|
||||||
func (s SourceDetail) View(st styles.Styles, focused bool) string {
|
|
||||||
inner := max(1, s.w-2)
|
|
||||||
rows := max(0, s.h-2)
|
|
||||||
offset := 0
|
|
||||||
if s.sel >= rows { // keep the cursor in view
|
|
||||||
offset = s.sel - rows + 1
|
|
||||||
}
|
|
||||||
var body []string
|
|
||||||
for i, f := range s.fields[min(offset, len(s.fields)):] {
|
|
||||||
if i >= rows {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
value := f.Value
|
|
||||||
if f.Info {
|
|
||||||
value = st.Muted.Render(value)
|
|
||||||
}
|
|
||||||
line := " " + st.Label.Render(fmt.Sprintf("%-10s", f.Label)) + " " + value
|
|
||||||
line = ansi.Truncate(line, inner, "…")
|
|
||||||
if i+offset == s.sel {
|
|
||||||
pad := strings.Repeat(" ", max(0, inner-ansi.StringWidth(line)))
|
|
||||||
if focused {
|
|
||||||
line = st.Selected.Render(ansi.Strip(line) + pad)
|
|
||||||
} else {
|
|
||||||
line = st.SelectedBlurred.Render(ansi.Strip(line) + pad)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
body = append(body, line)
|
|
||||||
}
|
|
||||||
footer := ""
|
|
||||||
if len(s.fields) > rows {
|
|
||||||
footer = fmt.Sprintf("%d of %d", s.sel+1, len(s.fields))
|
|
||||||
}
|
|
||||||
return Box(st, TitleStyle(st, focused).Render(s.title), footer, body, s.w, s.h, focused)
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user