fix: say a sign-in error once, then in Details for that account
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
87d98d038d
commit
b5c89bc6d9
3 files changed
+123
-15
No files matched your search
+65
-15
@@ -721,7 +721,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.waiting = 0
|
||||
m.askHandlerSetup()
|
||||
m.keys.Login.SetEnabled(m.auth != nil && len(m.auth.Login) > 0)
|
||||
m.keys.Login.SetEnabled(m.loginFor() != nil)
|
||||
m.detail.SetMessage(m.selectedProblem())
|
||||
if m.anyLoading() && !m.spinning {
|
||||
m.spinning = true
|
||||
cmds = append(cmds, m.spin.Tick)
|
||||
@@ -881,8 +882,8 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) (cmd tea.Cmd, quit bool) {
|
||||
case key.Matches(msg, k.Edit):
|
||||
return m.edit(), false
|
||||
case key.Matches(msg, k.Login):
|
||||
if m.auth != nil && len(m.auth.Login) > 0 {
|
||||
c := exec.Command(m.auth.Login[0], m.auth.Login[1:]...)
|
||||
if auth := m.loginFor(); auth != nil {
|
||||
c := exec.Command(auth.Login[0], auth.Login[1:]...)
|
||||
return tea.ExecProcess(c, func(err error) tea.Msg { return loginDoneMsg{err: err} }), false
|
||||
}
|
||||
case key.Matches(msg, k.Refresh):
|
||||
@@ -1261,7 +1262,14 @@ func (m *Model) handleSources(msg sourcesMsg) tea.Cmd {
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", p.Label(), r.err))
|
||||
}
|
||||
}
|
||||
m.status = strings.Join(errs, " · ")
|
||||
// Said once, when they change; after that the Details pane says it
|
||||
// for the selected account.
|
||||
if status := strings.Join(errs, " · "); status != m.status {
|
||||
m.status = status
|
||||
if status != "" {
|
||||
m.flash = flash{text: m.problemText(msg.provider, status), isErr: true}
|
||||
}
|
||||
}
|
||||
m.rebuildSources()
|
||||
m.sources.Loading = m.sourcesPending > 0
|
||||
return m.autoLoad(msg)
|
||||
@@ -1532,14 +1540,27 @@ func (m *Model) openSelected() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// noteErr remembers authentication failures so the UI can offer sign-in.
|
||||
// noteErr remembers authentication failures so the UI can offer sign-in,
|
||||
// saying so once for each account.
|
||||
func (m *Model) noteErr(err error) {
|
||||
var auth *provider.AuthError
|
||||
if errors.As(err, &auth) {
|
||||
if m.auth == nil || m.auth.Who() != auth.Who() {
|
||||
m.flash = flash{text: m.authText(auth), isErr: true}
|
||||
}
|
||||
m.auth = auth
|
||||
}
|
||||
}
|
||||
|
||||
// authText says which account needs signing in, and how.
|
||||
func (m Model) authText(auth *provider.AuthError) string {
|
||||
if len(auth.Login) == 0 {
|
||||
return fmt.Sprintf("%s: sign-in required: %v", auth.Who(), auth.Err)
|
||||
}
|
||||
return fmt.Sprintf("%s: sign-in required (not signed in or session expired)%s",
|
||||
auth.Who(), withKey(m.keys.Login, " · press %s to run "+strings.Join(auth.Login, " ")))
|
||||
}
|
||||
|
||||
func (m Model) anyLoading() bool {
|
||||
return m.sources.Loading || m.containers.Loading || m.secrets.Loading ||
|
||||
m.detail.State() == panes.ValueLoading
|
||||
@@ -1560,16 +1581,6 @@ func (m Model) footerView() string {
|
||||
help = m.helpBar(contextHelp{k: m.keys, focus: m.focus, commands: len(m.commandsHere()) > 0}.ShortHelp(), 2)
|
||||
}
|
||||
var lines []string
|
||||
failed := func(text string) { lines = append(lines, m.flashLine(flash{text: text, isErr: true})) }
|
||||
switch {
|
||||
case m.auth != nil && len(m.auth.Login) > 0:
|
||||
failed(fmt.Sprintf("%s: sign-in required (not signed in or session expired)%s",
|
||||
m.auth.Who(), withKey(m.keys.Login, " · press %s to run "+strings.Join(m.auth.Login, " "))))
|
||||
case m.auth != nil:
|
||||
failed(fmt.Sprintf("%s: sign-in required: %v", m.auth.Who(), m.auth.Err))
|
||||
case m.status != "":
|
||||
failed(m.status)
|
||||
}
|
||||
if !m.clipDeadline.IsZero() {
|
||||
left := max(0, int(math.Ceil(m.clipDeadline.Sub(m.now()).Seconds())))
|
||||
lines = append(lines, m.flashLine(done(fmt.Sprintf("copied %s · clipboard clears in %ds", m.clipLabel, left))))
|
||||
@@ -1624,3 +1635,42 @@ func (m Model) View() tea.View {
|
||||
v.WindowTitle = "lazysecrets"
|
||||
return v
|
||||
}
|
||||
|
||||
// selectedProblem is why the selected Sources row's account has nothing
|
||||
// to show, if it failed: for the Details pane.
|
||||
func (m Model) selectedProblem() string {
|
||||
r, ok := m.sources.Selected()
|
||||
if !ok || (r.kind != rowGroup && r.kind != rowSource) {
|
||||
return ""
|
||||
}
|
||||
res, done := m.sourceResults[r.provider]
|
||||
if !done || res.err == nil {
|
||||
return ""
|
||||
}
|
||||
return m.problemText(r.provider, res.err.Error())
|
||||
}
|
||||
|
||||
// problemText says what went wrong for a provider: for a sign-in, which
|
||||
// account and how to sign in again; else err as given.
|
||||
func (m Model) problemText(providerID, err string) string {
|
||||
var auth *provider.AuthError
|
||||
if r, ok := m.sourceResults[providerID]; ok && errors.As(r.err, &auth) {
|
||||
return m.authText(auth)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// loginFor is the sign-in L runs: the selected account's, if it needs
|
||||
// one, else the last one asked for.
|
||||
func (m Model) loginFor() *provider.AuthError {
|
||||
if r, ok := m.sources.Selected(); ok {
|
||||
var auth *provider.AuthError
|
||||
if res := m.sourceResults[r.provider]; errors.As(res.err, &auth) && len(auth.Login) > 0 {
|
||||
return auth
|
||||
}
|
||||
}
|
||||
if m.auth != nil && len(m.auth.Login) > 0 {
|
||||
return m.auth
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1022,3 +1022,50 @@ func TestMultiLineErrorsShownWhole(t *testing.T) {
|
||||
t.Errorf("footer:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
|
||||
// signedOut is a second Azure account whose sign-in has expired.
|
||||
type signedOut struct{ provider.Provider }
|
||||
|
||||
func (signedOut) ID() string { return "Azure:admin@contoso.com/t1" }
|
||||
func (signedOut) Label() string { return "Azure · admin" }
|
||||
func (signedOut) Sources(context.Context) ([]provider.Source, error) {
|
||||
return nil, &provider.AuthError{Kind: provider.KindAzure, Account: "Azure · admin",
|
||||
Login: []string{"az", "login", "--tenant", "t1"}, Err: errors.New("AADSTS70043")}
|
||||
}
|
||||
|
||||
func TestSignInMessageOnceThenInDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{CollapseGroups: false}, fake.Azure(0), signedOut{fake.Azure(0)})
|
||||
hint := "Azure · admin: sign-in required"
|
||||
if !strings.Contains(lastLines(d.screen(), 2), hint) {
|
||||
t.Fatalf("not said at start:\n%s", d.screen())
|
||||
}
|
||||
d.keys("j")
|
||||
if strings.Contains(lastLines(d.screen(), 2), hint) {
|
||||
t.Errorf("still in the footer after moving:\n%s", d.screen())
|
||||
}
|
||||
|
||||
// On the account's group, Details says it, and L signs in to it.
|
||||
d.keys("G")
|
||||
if r, _ := d.m.sources.Selected(); r.provider != "Azure:admin@contoso.com/t1" {
|
||||
t.Fatalf("selected %+v", r)
|
||||
}
|
||||
if !strings.Contains(d.screen(), hint) || !strings.Contains(d.screen(), "press L to run az login --tenant t1") {
|
||||
t.Errorf("not in Details:\n%s", d.screen())
|
||||
}
|
||||
if a := d.m.loginFor(); a == nil || a.Account != "Azure · admin" {
|
||||
t.Errorf("L signs in to %+v", a)
|
||||
}
|
||||
|
||||
// Elsewhere Details is as usual.
|
||||
d.keys("g")
|
||||
if strings.Contains(d.screen(), hint) {
|
||||
t.Errorf("shown for another account:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
|
||||
// lastLines is the bottom n lines of a screen.
|
||||
func lastLines(scr string, n int) string {
|
||||
lines := strings.Split(strings.TrimRight(scr, "\n"), "\n")
|
||||
return strings.Join(lines[max(0, len(lines)-n):], "\n")
|
||||
}
|
||||
@@ -46,6 +46,7 @@ type Detail struct {
|
||||
|
||||
versions []provider.Object // newest first; nil while unknown
|
||||
note string // the user's note on what's shown
|
||||
message string // shown instead of the hint when nothing is selected: an error
|
||||
|
||||
offset int
|
||||
w, h int
|
||||
@@ -71,6 +72,10 @@ func (d *Detail) Clear() {
|
||||
*d = Detail{Now: d.Now, Icons: d.Icons, RevealKey: d.RevealKey, VersionsKey: d.VersionsKey, w: d.w, h: d.h} // drops the note too
|
||||
}
|
||||
|
||||
// SetMessage sets what's shown instead of the hint when nothing is
|
||||
// selected, such as why an account has nothing to show; "" for the hint.
|
||||
func (d *Detail) SetMessage(s string) { d.message = s }
|
||||
|
||||
// SetDescribing marks extra metadata as being fetched.
|
||||
func (d *Detail) SetDescribing() { d.describing, d.describeErr = true, nil }
|
||||
|
||||
@@ -186,6 +191,12 @@ func (d Detail) lines(st styles.Styles) []string {
|
||||
muted := st.Muted.Render
|
||||
|
||||
if d.container == nil {
|
||||
if d.message != "" {
|
||||
for _, l := range strings.Split(ansi.Hardwrap(d.message, max(1, inner-2), true), "\n") {
|
||||
out = append(out, " "+st.Error.Render(l))
|
||||
}
|
||||
return out
|
||||
}
|
||||
return []string{muted(" Select a vault or scope to browse its secrets.")}
|
||||
}
|
||||
c := *d.container
|
||||
|
||||
Reference in new issue
Block a user