-
Notifications
You must be signed in to change notification settings - Fork 167
auth: highlight default profile and unify pickers across login/logout/switch/token #5218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e86dbe1
1580065
99094db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| package auth | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "strings" | ||
|
|
||
| "github.com/databricks/cli/libs/cmdio" | ||
| "github.com/databricks/cli/libs/databrickscfg/profile" | ||
| ) | ||
|
|
||
| // profilePickerResult represents the user's choice from pickAuthProfile. | ||
| type profilePickerResult int | ||
|
|
||
| const ( | ||
| profilePickerProfile profilePickerResult = iota // an existing profile was picked | ||
| profilePickerCreateNew // user chose "Create a new profile" | ||
| profilePickerEnterHost // user chose "Enter a host URL manually" | ||
| ) | ||
|
|
||
| const ( | ||
| profilePickerCreateNewLabel = "Create a new profile" | ||
| profilePickerEnterHostLabel = "Enter a host URL manually" | ||
| ) | ||
|
|
||
| // profilePickerOptions configures pickAuthProfile. | ||
| type profilePickerOptions struct { | ||
| // Label shown above the picker. | ||
| Label string | ||
|
|
||
| // SelectedNoun is the noun shown after selection ("Using profile", | ||
| // "Selected profile", "Default profile"). Defaults to "Using profile". | ||
| SelectedNoun string | ||
|
|
||
| // Default is the name of the default profile. When set, it is moved to the | ||
| // top of the list and decorated with "[default]". | ||
| Default string | ||
|
|
||
| // IncludeExtras appends "Create a new profile" and "Enter a host URL | ||
| // manually" entries after the profile list. Picker action entries are | ||
| // shown even when the profile list is empty. | ||
| IncludeExtras bool | ||
| } | ||
|
|
||
| // pickerItem is a single entry rendered by the picker. It can be either a real | ||
| // profile or one of the extra action entries (Create new / Enter host). | ||
| type pickerItem struct { | ||
| Name string | ||
| Host string | ||
| AccountID string | ||
| IsDefault bool | ||
|
|
||
| // IsExtra distinguishes action entries (Create new / Enter host) from | ||
| // real profiles, so a profile that happens to share a label name still | ||
| // resolves correctly. | ||
| IsExtra bool | ||
| Extra profilePickerResult | ||
| } | ||
|
|
||
| // buildPickerItems returns the items shown by pickAuthProfile, with the default | ||
| // profile moved to the top and the extras appended (when requested). | ||
| func buildPickerItems(profiles profile.Profiles, defaultName string, includeExtras bool) []pickerItem { | ||
| defaultIdx := -1 | ||
| if defaultName != "" { | ||
| for i, p := range profiles { | ||
| if p.Name == defaultName { | ||
| defaultIdx = i | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| itemFor := func(p profile.Profile, isDefault bool) pickerItem { | ||
| return pickerItem{ | ||
| Name: p.Name, | ||
| Host: p.Host, | ||
| AccountID: p.AccountID, | ||
| IsDefault: isDefault, | ||
| } | ||
| } | ||
|
|
||
| items := make([]pickerItem, 0, len(profiles)+2) | ||
| if defaultIdx >= 0 { | ||
| items = append(items, itemFor(profiles[defaultIdx], true)) | ||
| } | ||
| for i, p := range profiles { | ||
| if i == defaultIdx { | ||
| continue | ||
| } | ||
| items = append(items, itemFor(p, false)) | ||
| } | ||
| if includeExtras { | ||
| items = append(items, | ||
| pickerItem{Name: profilePickerCreateNewLabel, IsExtra: true, Extra: profilePickerCreateNew}, | ||
| pickerItem{Name: profilePickerEnterHostLabel, IsExtra: true, Extra: profilePickerEnterHost}, | ||
| ) | ||
| } | ||
|
Comment on lines
+92
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Question] From the point of view of the semantics of these commands it makes sense to have the extras at the bottom. You call Is this something we should acknowledge but accept or should we move the extra options at the top and then have the profiles bellow?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The question is ultimately, do we think people are more likely to use databricks auth login to refresh tokens for existing profiles or to create new profiles. My assumption is that the more profiles you have, the less likely you are to want to create new profiles (and also the more advanced a user you would be). So thats why the behavior is how it is right now |
||
| return items | ||
| } | ||
|
|
||
| // pickAuthProfile shows the auth profile picker and returns the user's choice. | ||
| // When the result is profilePickerProfile, the second return value is the | ||
| // selected profile name. For the other results it is empty. | ||
| func pickAuthProfile(ctx context.Context, profiles profile.Profiles, opts profilePickerOptions) (profilePickerResult, string, error) { | ||
| items := buildPickerItems(profiles, opts.Default, opts.IncludeExtras) | ||
| if len(items) == 0 { | ||
| return 0, "", errors.New("no profiles configured. Run 'databricks auth login' to create a profile") | ||
| } | ||
| noun := opts.SelectedNoun | ||
| if noun == "" { | ||
| noun = "Using profile" | ||
| } | ||
|
|
||
| idx, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ | ||
| Label: opts.Label, | ||
| Items: items, | ||
| StartInSearchMode: len(profiles) > 5, | ||
| Searcher: func(input string, index int) bool { | ||
| input = strings.ToLower(input) | ||
| return strings.Contains(strings.ToLower(items[index].Name), input) || | ||
| strings.Contains(strings.ToLower(items[index].Host), input) || | ||
| strings.Contains(strings.ToLower(items[index].AccountID), input) | ||
| }, | ||
| LabelTemplate: "{{ . | faint }}", | ||
| Active: `▸ {{.Name | bold}}{{if .IsDefault}} {{ "[default]" | green }}{{end}}{{if .AccountID}} (account: {{.AccountID|faint}}){{else if .Host}} ({{.Host|faint}}){{end}}`, | ||
| Inactive: ` {{.Name}}{{if .IsDefault}} [default]{{end}}{{if .AccountID}} (account: {{.AccountID|faint}}){{else if .Host}} ({{.Host|faint}}){{end}}`, | ||
| Selected: `{{ "` + noun + `" | faint }}: {{ .Name | bold }}`, | ||
| }) | ||
| if err != nil { | ||
| return 0, "", err | ||
| } | ||
|
|
||
| picked := items[idx] | ||
| if picked.IsExtra { | ||
| return picked.Extra, "", nil | ||
| } | ||
| return profilePickerProfile, picked.Name, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package auth | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/databricks/cli/libs/databrickscfg/profile" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestBuildPickerItems(t *testing.T) { | ||
| profiles := profile.Profiles{ | ||
| {Name: "alpha", Host: "https://alpha.cloud.databricks.example"}, | ||
| {Name: "bravo", Host: "https://bravo.cloud.databricks.example"}, | ||
| {Name: "charlie", Host: "https://charlie.cloud.databricks.example"}, | ||
| } | ||
|
|
||
| cases := []struct { | ||
| name string | ||
| defaultName string | ||
| includeExtras bool | ||
| wantNames []string | ||
| wantDefault string | ||
| wantExtras []profilePickerResult | ||
| }{ | ||
| { | ||
| name: "no default no extras", | ||
| wantNames: []string{"alpha", "bravo", "charlie"}, | ||
| wantDefault: "", | ||
| }, | ||
| { | ||
| name: "default moves to top", | ||
| defaultName: "bravo", | ||
| wantNames: []string{"bravo", "alpha", "charlie"}, | ||
| wantDefault: "bravo", | ||
| }, | ||
| { | ||
| name: "extras appended after profiles", | ||
| includeExtras: true, | ||
| wantNames: []string{"alpha", "bravo", "charlie", profilePickerCreateNewLabel, profilePickerEnterHostLabel}, | ||
| wantExtras: []profilePickerResult{profilePickerCreateNew, profilePickerEnterHost}, | ||
| }, | ||
| { | ||
| name: "default first, then extras at the bottom", | ||
| defaultName: "charlie", | ||
| includeExtras: true, | ||
| wantNames: []string{"charlie", "alpha", "bravo", profilePickerCreateNewLabel, profilePickerEnterHostLabel}, | ||
| wantDefault: "charlie", | ||
| wantExtras: []profilePickerResult{profilePickerCreateNew, profilePickerEnterHost}, | ||
| }, | ||
| { | ||
| name: "default not in profiles is ignored", | ||
| defaultName: "missing", | ||
| wantNames: []string{"alpha", "bravo", "charlie"}, | ||
| wantDefault: "", | ||
| }, | ||
| } | ||
|
|
||
| t.Run("empty profiles with extras shows only extras", func(t *testing.T) { | ||
| items := buildPickerItems(profile.Profiles{}, "", true) | ||
| assert.Equal(t, []string{profilePickerCreateNewLabel, profilePickerEnterHostLabel}, namesOf(items)) | ||
| }) | ||
|
|
||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| items := buildPickerItems(profiles, tc.defaultName, tc.includeExtras) | ||
|
|
||
| gotDefault := "" | ||
| var gotExtras []profilePickerResult | ||
| for _, it := range items { | ||
| if it.IsDefault { | ||
| assert.Empty(t, gotDefault) | ||
| gotDefault = it.Name | ||
| } | ||
| if it.IsExtra { | ||
| gotExtras = append(gotExtras, it.Extra) | ||
| } | ||
| } | ||
| assert.Equal(t, tc.wantNames, namesOf(items)) | ||
| assert.Equal(t, tc.wantDefault, gotDefault) | ||
| assert.Equal(t, tc.wantExtras, gotExtras) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func namesOf(items []pickerItem) []string { | ||
| names := make([]string, len(items)) | ||
| for i, it := range items { | ||
| names[i] = it.Name | ||
| } | ||
| return names | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Question] Do we want this to fall through to the profile name prompt bellow?
From playing around if I introduce the host of a workspace for which I already have a profile I get asked to input a profile name and it creates a new profile.
Is this what we want or should we have some discovery mechanism where we show the available profiles for that host?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we want to keep it simple, and assume you want to create a new profile in this case