|
| 1 | +package auth |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "errors" |
| 6 | + "strings" |
| 7 | + |
| 8 | + "github.com/databricks/cli/libs/cmdio" |
| 9 | + "github.com/databricks/cli/libs/databrickscfg/profile" |
| 10 | +) |
| 11 | + |
| 12 | +// profilePickerResult represents the user's choice from pickAuthProfile. |
| 13 | +type profilePickerResult int |
| 14 | + |
| 15 | +const ( |
| 16 | + profilePickerProfile profilePickerResult = iota // an existing profile was picked |
| 17 | + profilePickerCreateNew // user chose "Create a new profile" |
| 18 | + profilePickerEnterHost // user chose "Enter a host URL manually" |
| 19 | +) |
| 20 | + |
| 21 | +const ( |
| 22 | + profilePickerCreateNewLabel = "Create a new profile" |
| 23 | + profilePickerEnterHostLabel = "Enter a host URL manually" |
| 24 | +) |
| 25 | + |
| 26 | +// profilePickerOptions configures pickAuthProfile. |
| 27 | +type profilePickerOptions struct { |
| 28 | + // Label shown above the picker. |
| 29 | + Label string |
| 30 | + |
| 31 | + // SelectedNoun is the noun shown after selection ("Using profile", |
| 32 | + // "Selected profile", "Default profile"). Defaults to "Using profile". |
| 33 | + SelectedNoun string |
| 34 | + |
| 35 | + // Default is the name of the default profile. When set, it is moved to the |
| 36 | + // top of the list and decorated with "[default]". |
| 37 | + Default string |
| 38 | + |
| 39 | + // IncludeExtras appends "Create a new profile" and "Enter a host URL |
| 40 | + // manually" entries after the profile list. Picker action entries are |
| 41 | + // shown even when the profile list is empty. |
| 42 | + IncludeExtras bool |
| 43 | +} |
| 44 | + |
| 45 | +// pickerItem is a single entry rendered by the picker. It can be either a real |
| 46 | +// profile or one of the extra action entries (Create new / Enter host). |
| 47 | +type pickerItem struct { |
| 48 | + Name string |
| 49 | + Host string |
| 50 | + AccountID string |
| 51 | + IsDefault bool |
| 52 | + |
| 53 | + // IsExtra distinguishes action entries (Create new / Enter host) from |
| 54 | + // real profiles, so a profile that happens to share a label name still |
| 55 | + // resolves correctly. |
| 56 | + IsExtra bool |
| 57 | + Extra profilePickerResult |
| 58 | +} |
| 59 | + |
| 60 | +// buildPickerItems returns the items shown by pickAuthProfile, with the default |
| 61 | +// profile moved to the top and the extras appended (when requested). |
| 62 | +func buildPickerItems(profiles profile.Profiles, defaultName string, includeExtras bool) []pickerItem { |
| 63 | + defaultIdx := -1 |
| 64 | + if defaultName != "" { |
| 65 | + for i, p := range profiles { |
| 66 | + if p.Name == defaultName { |
| 67 | + defaultIdx = i |
| 68 | + break |
| 69 | + } |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + itemFor := func(p profile.Profile, isDefault bool) pickerItem { |
| 74 | + return pickerItem{ |
| 75 | + Name: p.Name, |
| 76 | + Host: p.Host, |
| 77 | + AccountID: p.AccountID, |
| 78 | + IsDefault: isDefault, |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + items := make([]pickerItem, 0, len(profiles)+2) |
| 83 | + if defaultIdx >= 0 { |
| 84 | + items = append(items, itemFor(profiles[defaultIdx], true)) |
| 85 | + } |
| 86 | + for i, p := range profiles { |
| 87 | + if i == defaultIdx { |
| 88 | + continue |
| 89 | + } |
| 90 | + items = append(items, itemFor(p, false)) |
| 91 | + } |
| 92 | + if includeExtras { |
| 93 | + items = append(items, |
| 94 | + pickerItem{Name: profilePickerCreateNewLabel, IsExtra: true, Extra: profilePickerCreateNew}, |
| 95 | + pickerItem{Name: profilePickerEnterHostLabel, IsExtra: true, Extra: profilePickerEnterHost}, |
| 96 | + ) |
| 97 | + } |
| 98 | + return items |
| 99 | +} |
| 100 | + |
| 101 | +// pickAuthProfile shows the auth profile picker and returns the user's choice. |
| 102 | +// When the result is profilePickerProfile, the second return value is the |
| 103 | +// selected profile name. For the other results it is empty. |
| 104 | +func pickAuthProfile(ctx context.Context, profiles profile.Profiles, opts profilePickerOptions) (profilePickerResult, string, error) { |
| 105 | + items := buildPickerItems(profiles, opts.Default, opts.IncludeExtras) |
| 106 | + if len(items) == 0 { |
| 107 | + return 0, "", errors.New("no profiles configured. Run 'databricks auth login' to create a profile") |
| 108 | + } |
| 109 | + noun := opts.SelectedNoun |
| 110 | + if noun == "" { |
| 111 | + noun = "Using profile" |
| 112 | + } |
| 113 | + |
| 114 | + idx, err := cmdio.RunSelect(ctx, cmdio.SelectOptions{ |
| 115 | + Label: opts.Label, |
| 116 | + Items: items, |
| 117 | + StartInSearchMode: len(profiles) > 5, |
| 118 | + Searcher: func(input string, index int) bool { |
| 119 | + input = strings.ToLower(input) |
| 120 | + return strings.Contains(strings.ToLower(items[index].Name), input) || |
| 121 | + strings.Contains(strings.ToLower(items[index].Host), input) || |
| 122 | + strings.Contains(strings.ToLower(items[index].AccountID), input) |
| 123 | + }, |
| 124 | + LabelTemplate: "{{ . | faint }}", |
| 125 | + Active: `▸ {{.Name | bold}}{{if .IsDefault}} {{ "[default]" | green }}{{end}}{{if .AccountID}} (account: {{.AccountID|faint}}){{else if .Host}} ({{.Host|faint}}){{end}}`, |
| 126 | + Inactive: ` {{.Name}}{{if .IsDefault}} [default]{{end}}{{if .AccountID}} (account: {{.AccountID|faint}}){{else if .Host}} ({{.Host|faint}}){{end}}`, |
| 127 | + Selected: `{{ "` + noun + `" | faint }}: {{ .Name | bold }}`, |
| 128 | + }) |
| 129 | + if err != nil { |
| 130 | + return 0, "", err |
| 131 | + } |
| 132 | + |
| 133 | + picked := items[idx] |
| 134 | + if picked.IsExtra { |
| 135 | + return picked.Extra, "", nil |
| 136 | + } |
| 137 | + return profilePickerProfile, picked.Name, nil |
| 138 | +} |
0 commit comments