diff --git a/README.md b/README.md index 3d5574d..f538a1d 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ lazysecrets # start lazysecrets lazysecrets --demo # test with demo data ``` -Pick a source on the left, then a vault, then a secret. The keys you'll +Pick a source on the left, then a vault, then a secret. With nothing +signed in yet, the right pane says how to get started. The keys you'll use most: | Key | Action | diff --git a/internal/ui/app/app_test.go b/internal/ui/app/app_test.go index 3f1236d..a2e53f2 100644 --- a/internal/ui/app/app_test.go +++ b/internal/ui/app/app_test.go @@ -1238,3 +1238,36 @@ func TestStatusPane(t *testing.T) { t.Errorf("back to %v", d.m.focus) } } + +// empty is an account with nothing in it, like Databricks without profiles. +type empty struct{ provider.Provider } + +func (empty) Sources(context.Context) ([]provider.Source, error) { return nil, nil } + +func TestGetStarted(t *testing.T) { + t.Parallel() + d := newDriverOpts(t, Options{}, signedOut{fake.Azure(0)}, empty{fake.Databricks(0)}) + scr := d.screen() + for _, want := range []string{"get started", "Azure az login", "L runs it here", + "databricks auth login --host", "lazysecrets --demo"} { + if !strings.Contains(scr, want) { + t.Errorf("missing %q:\n%s", want, scr) + } + } + d.keys("l", "j") + if a := d.m.loginFor(); a == nil || a.Account != "Azure ยท admin" { + t.Errorf("L signs in to %+v", a) + } + d.keys("y") + if got := d.clip().content; got != "az login" { + t.Errorf("y copied %q", got) + } + + // Once an account has something, an empty one says what it needs. + d = newDriverOpts(t, Options{}, fake.Azure(0), empty{fake.Databricks(0)}) + d.m.sources.SelectWhere(func(r sourceRow) bool { return r.kind == rowGroup && r.provider == fake.Databricks(0).ID() }) + d.send(nil) + if scr := d.screen(); strings.Contains(scr, "get started") || !strings.Contains(scr, "databricks auth login") { + t.Errorf("empty Databricks:\n%s", scr) + } +} diff --git a/internal/ui/app/fields.go b/internal/ui/app/fields.go index 6a0f253..16574f7 100644 --- a/internal/ui/app/fields.go +++ b/internal/ui/app/fields.go @@ -53,6 +53,8 @@ func (m *Model) syncFields() tea.Cmd { case paneSources: r, ok := m.sources.Selected() switch { + case m.nothingFound(): + m.fields.Show("get started", m.getStartedFields()) case !ok: m.fields.Show("", nil) case m.selectedProblem() != "": @@ -106,6 +108,10 @@ func (m *Model) showAccount(r sourceRow) tea.Cmd { } fields = append(fields, groupFields(st)...) } + if res, done := m.sourceResults[r.provider]; done && res.err == nil && len(res.sources) == 0 { + hint := emptyHint(p.Kind()) + fields = append(fields, panes.Field{Label: "nothing", Value: "found: try", Info: true}, panes.Field{Value: hint.Value, URL: hint.URL}) + } m.fields.Show(p.Label(), fields) return cmd } diff --git a/internal/ui/app/getstarted.go b/internal/ui/app/getstarted.go new file mode 100644 index 0000000..382cd5b --- /dev/null +++ b/internal/ui/app/getstarted.go @@ -0,0 +1,66 @@ +package app + +import ( + "errors" + + "git.kebcloud.no/keb/lazysecrets/internal/provider" + "git.kebcloud.no/keb/lazysecrets/internal/ui/panes" + "git.kebcloud.no/keb/lazysecrets/internal/update" +) + +// When no account has anything to show, as on a first run, the right +// pane says how to get started: the sign-in for each kind of account +// (y copies it, L runs az login), and the demo. + +// nothingFound reports whether every account has loaded, with nothing. +func (m Model) nothingFound() bool { + if m.sourcesPending > 0 { + return false + } + for _, p := range m.providers { + if len(m.sourceResults[p.ID()].sources) > 0 { + return false + } + } + return true +} + +// getStartedFields are the rows saying how to get started. +func (m Model) getStartedFields() []panes.Field { + out := []panes.Field{{Value: "Nothing to show yet: sign in where your secrets are.", Info: true}} + seen := map[provider.Kind]bool{} + for _, p := range m.providers { + if seen[p.Kind()] { + continue + } + seen[p.Kind()] = true + f := emptyHint(p.Kind()) + var auth *provider.AuthError + switch err := m.sourceResults[p.ID()].err; { + case errors.As(err, &auth) && len(auth.Login) > 0: + f.Provider = p.ID() // L signs in + out = append(out, f, panes.Field{Value: withKey(m.keys.Login, "%s runs it here"), Info: true}) + case err != nil: + out = append(out, f, panes.Field{Value: err.Error(), Info: true, Err: true}) + default: + out = append(out, f) + } + } + return append(out, + panes.Field{Label: "try", Value: "lazysecrets --demo"}, + panes.Field{Value: "built-in sample data, to look around", Info: true}, + panes.Field{Label: "docs", Value: "getting started", URL: update.DefaultRepo + "#get-started"}, + ) +} + +// emptyHint is how an account of kind gets something to show. +func emptyHint(kind provider.Kind) panes.Field { + switch kind { + case provider.KindDatabricks: + return panes.Field{Label: "Databricks", Value: "databricks auth login --host https://"} + case provider.KindRepo: + return panes.Field{Label: "repos", Value: "SOPS and .env files in your git repos", + URL: update.DefaultRepo + "/src/branch/main/docs/repos.md"} + } + return panes.Field{Label: string(kind), Value: "az login"} +}