feat: vaults, scopes and files show their details: a vault's settings, who has a scope, a file's keys and git status
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9b0329e9da
commit
5381cf5d43
14 files changed
+474
-2
No files matched your search
@@ -177,6 +177,7 @@ type Model struct {
|
||||
fields panes.Fields
|
||||
lastLeft pane
|
||||
groups map[string]groupsState
|
||||
details map[string]detailsState // by containerKey
|
||||
getenv func(string) string
|
||||
nvimOpen func(server, path string, line int) error // edit.go
|
||||
now func() time.Time
|
||||
@@ -376,6 +377,7 @@ func New(providers []provider.Provider, opts Options) Model {
|
||||
nvimOpen: nvimOpen,
|
||||
cache: newCache(),
|
||||
groups: map[string]groupsState{},
|
||||
details: map[string]detailsState{},
|
||||
fields: panes.NewFields(),
|
||||
}
|
||||
if m.clip == nil {
|
||||
@@ -588,6 +590,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case groupsMsg:
|
||||
m.groups[msg.provider] = groupsState{names: msg.names, err: msg.err}
|
||||
|
||||
case detailsMsg:
|
||||
m.details[msg.key] = detailsState{attrs: msg.attrs, err: msg.err}
|
||||
|
||||
case crawlMsg:
|
||||
cmds = append(cmds, m.handleCrawl(msg))
|
||||
|
||||
@@ -1032,6 +1037,7 @@ func (m *Model) refresh() tea.Cmd {
|
||||
case paneContainers:
|
||||
if src, ok := m.selectedSource(); ok {
|
||||
m.containers.Refresh()
|
||||
m.details = map[string]detailsState{}
|
||||
return m.loadContainers(src, m.selSource)
|
||||
}
|
||||
case paneSecrets:
|
||||
|
||||
@@ -1236,3 +1236,22 @@ func TestStatusPane(t *testing.T) {
|
||||
t.Errorf("back to %v", d.m.focus)
|
||||
}
|
||||
}
|
||||
|
||||
// detailed is an account whose vaults have details.
|
||||
type detailed struct{ provider.Provider }
|
||||
|
||||
func (detailed) Details(_ context.Context, c provider.Container) ([]provider.Attr, error) {
|
||||
return []provider.Attr{{Label: "access", Value: "Azure roles (RBAC)"}, {Label: "git", Value: "tracked", Warn: true}}, nil
|
||||
}
|
||||
|
||||
func TestContainerDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{}, detailed{fake.Azure(0)})
|
||||
d.keys("2")
|
||||
if scr := d.screen(); !strings.Contains(scr, "access Azure roles (RBAC)") || !strings.Contains(scr, "URI") {
|
||||
t.Errorf("details:\n%s", scr)
|
||||
}
|
||||
if st := d.m.details[d.m.containers.SelectedID()]; st.loading || len(st.attrs) != 2 {
|
||||
t.Errorf("state %+v", st)
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,20 @@ type groupsMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// A vault's, scope's or file's details beyond its listing load once a
|
||||
// session, the first time it's shown, and again with refresh.
|
||||
type detailsState struct {
|
||||
attrs []provider.Attr
|
||||
err error
|
||||
loading bool
|
||||
}
|
||||
|
||||
type detailsMsg struct {
|
||||
key string
|
||||
attrs []provider.Attr
|
||||
err error
|
||||
}
|
||||
|
||||
// detailOf is the pane the fourth pane describes: the one in focus, or,
|
||||
// with the fourth in focus, the one it was entered from.
|
||||
func (m Model) detailOf() pane {
|
||||
@@ -68,7 +82,9 @@ func (m *Model) syncFields() tea.Cmd {
|
||||
m.fields.Show("lazysecrets", m.statusFields())
|
||||
case paneContainers:
|
||||
if c, ok := m.containers.Selected(); ok {
|
||||
cmd := m.loadDetails(c)
|
||||
m.fields.Show(c.Name, m.containerFields(c))
|
||||
return cmd
|
||||
} else {
|
||||
m.fields.Show("", nil)
|
||||
}
|
||||
@@ -171,9 +187,37 @@ func (m Model) containerFields(c provider.Container) []panes.Field {
|
||||
add(strings.ToLower(string(t))+"s", fmt.Sprint(len(e.value)))
|
||||
}
|
||||
}
|
||||
switch st := m.details[containerKey(c)]; {
|
||||
case st.loading:
|
||||
out = append(out, panes.Field{Label: "details", Value: "loading…", Info: true})
|
||||
case errors.Is(st.err, provider.ErrUnsupported):
|
||||
case st.err != nil:
|
||||
out = append(out, panes.Field{Label: "details", Value: "couldn't load them: " + st.err.Error(), Info: true, Err: true})
|
||||
}
|
||||
for _, a := range m.details[containerKey(c)].attrs {
|
||||
out = append(out, panes.Field{Label: a.Label, Value: a.Value, Err: a.Warn})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// loadDetails loads c's details, if its provider has them and they
|
||||
// aren't loaded yet.
|
||||
func (m *Model) loadDetails(c provider.Container) tea.Cmd {
|
||||
d, ok := m.providerFor(c.Source).(provider.ContainerDetails)
|
||||
key := containerKey(c)
|
||||
if _, seen := m.details[key]; !ok || seen {
|
||||
return nil
|
||||
}
|
||||
m.details[key] = detailsState{loading: true}
|
||||
timeout := m.timeout
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
attrs, err := d.Details(ctx, c)
|
||||
return detailsMsg{key: key, attrs: attrs, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// problemFields show why an account has nothing to show, wrapped to the
|
||||
// pane, in red.
|
||||
func (m Model) problemFields(problem string) []panes.Field {
|
||||
|
||||
Reference in new issue
Block a user