diff --git a/pkg/assume/assume.go b/pkg/assume/assume.go index 0cacd12b..0cb5da4f 100644 --- a/pkg/assume/assume.go +++ b/pkg/assume/assume.go @@ -777,21 +777,21 @@ func QueryProfiles(profiles *cfaws.Profiles) (string, error) { originalSelectTemplate := survey.SelectQuestionTemplate survey.SelectQuestionTemplate = fmt.Sprintf(` {{- define "option"}} -{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} -{{- .CurrentOpt.Value}} -{{- color "reset"}} + {{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} + {{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}} + {{- color "reset"}} {{end}} {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} {{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} {{- else}} -{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} -{{- "\n"}} -%s -{{- range $ix, $option := .PageEntries}} -{{- template "option" $.IterateOption $ix $option}} -{{- end}} + {{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} + {{- "\n"}} +%s{{- "\n"}} + {{- range $ix, $option := .PageEntries}} + {{- template "option" $.IterateOption $ix $option}} + {{- end}} {{- end}}`, promptHeader) clio.NewLine() diff --git a/pkg/granted/eks/eks.go b/pkg/granted/eks/eks.go index a9623198..e3c128aa 100644 --- a/pkg/granted/eks/eks.go +++ b/pkg/granted/eks/eks.go @@ -6,8 +6,6 @@ import ( "connectrpc.com/connect" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" "github.com/common-fate/clio" "github.com/common-fate/grab" "github.com/common-fate/granted/pkg/cfcfg" @@ -15,7 +13,6 @@ import ( "github.com/common-fate/sdk/config" accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" "github.com/common-fate/sdk/service/access" - "github.com/mattn/go-runewidth" "github.com/urfave/cli/v2" ) @@ -166,50 +163,5 @@ func promptForClusterAndRole(ctx context.Context, cfg *config.Context) (*accessv return nil, errors.New("you don't have access to any EKS Clusters") } - type Column struct { - Title string - Width int - } - cols := []Column{{Title: "Cluster", Width: 40}, {Title: "Role", Width: 40}} - var s = make([]string, 0, len(cols)) - for _, col := range cols { - style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) - renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) - s = append(s, lipgloss.NewStyle().Bold(true).Padding(0).Render(renderedCell)) - } - header := lipgloss.NewStyle().PaddingLeft(2).Render(lipgloss.JoinHorizontal(lipgloss.Left, s...)) - var options []huh.Option[*accessv1alpha1.Entitlement] - - for _, entitlement := range entitlements { - style := lipgloss.NewStyle().Width(cols[0].Width).MaxWidth(cols[0].Width).Inline(true) - target := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Target.Display(), cols[0].Width, "…"))) - - style = lipgloss.NewStyle().Width(cols[1].Width).MaxWidth(cols[1].Width).Inline(true) - role := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Role.Display(), cols[1].Width, "…"))) - - options = append(options, huh.Option[*accessv1alpha1.Entitlement]{ - Key: lipgloss.JoinHorizontal(lipgloss.Left, target, role), - Value: entitlement, - }) - } - - selector := huh.NewSelect[*accessv1alpha1.Entitlement](). - // show the filter dialog when there are 2 or more options - Filtering(len(options) > 1). - Options(options...). - Title("Select a cluster to connect to"). - Description(header).WithTheme(huh.ThemeBase()) - - err = selector.Run() - if err != nil { - return nil, err - } - - selectorVal := selector.GetValue() - - if selectorVal == nil { - return nil, errors.New("no cluster selected") - } - - return selectorVal.(*accessv1alpha1.Entitlement), nil + return proxy.PromptEntitlements(entitlements, "Cluster", "Service Account", "Select a cluster to connect to: ") } diff --git a/pkg/granted/proxy/prompt.go b/pkg/granted/proxy/prompt.go new file mode 100644 index 00000000..9a4f6493 --- /dev/null +++ b/pkg/granted/proxy/prompt.go @@ -0,0 +1,83 @@ +package proxy + +import ( + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/lipgloss" + accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" + "github.com/mattn/go-runewidth" +) + +func filterMultiToken(filterValue string, optValue string, optIndex int) bool { + optValue = strings.ToLower(optValue) + filters := strings.Split(strings.ToLower(filterValue), " ") + for _, filter := range filters { + if !strings.Contains(optValue, filter) { + return false + } + } + return true +} +func PromptEntitlements(entitlements []*accessv1alpha1.Entitlement, targetHeader string, roleHeader string, promptMessage string) (*accessv1alpha1.Entitlement, error) { + type Column struct { + Title string + Width int + } + cols := []Column{{Title: targetHeader, Width: 40}, {Title: roleHeader, Width: 40}} + var s = make([]string, 0, len(cols)) + for _, col := range cols { + style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) + renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) + s = append(s, lipgloss.NewStyle().Bold(true).Padding(0).Render(renderedCell)) + } + header := lipgloss.NewStyle().PaddingLeft(2).Render(lipgloss.JoinHorizontal(lipgloss.Left, s...)) + var options []string + optionsMap := make(map[string]*accessv1alpha1.Entitlement) + for i, entitlement := range entitlements { + style := lipgloss.NewStyle().Width(cols[0].Width).MaxWidth(cols[0].Width).Inline(true) + target := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Target.Display(), cols[0].Width, "…"))) + + style = lipgloss.NewStyle().Width(cols[1].Width).MaxWidth(cols[1].Width).Inline(true) + role := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Role.Display(), cols[1].Width, "…"))) + + option := lipgloss.JoinHorizontal(lipgloss.Left, target, role) + options = append(options, option) + optionsMap[option] = entitlements[i] + } + + originalSelectTemplate := survey.SelectQuestionTemplate + survey.SelectQuestionTemplate = fmt.Sprintf(` +{{- define "option"}} + {{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} + {{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}} + {{- color "reset"}} +{{end}} +{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} +{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} +{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} +{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} +{{- else}} + {{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} + {{- "\n"}} +%s{{- "\n"}} + {{- range $ix, $option := .PageEntries}} + {{- template "option" $.IterateOption $ix $option}} + {{- end}} +{{- end}}`, header) + + var out string + err := survey.AskOne(&survey.Select{ + Message: promptMessage, + Options: options, + Filter: filterMultiToken, + }, &out) + if err != nil { + return nil, err + } + + survey.SelectQuestionTemplate = originalSelectTemplate + + return optionsMap[out], nil +} diff --git a/pkg/granted/rds/rds.go b/pkg/granted/rds/rds.go index b94d59fa..d13d94b6 100644 --- a/pkg/granted/rds/rds.go +++ b/pkg/granted/rds/rds.go @@ -7,8 +7,6 @@ import ( "connectrpc.com/connect" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" "github.com/common-fate/clio" "github.com/common-fate/grab" "github.com/common-fate/granted/pkg/cfcfg" @@ -17,7 +15,6 @@ import ( accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" "github.com/common-fate/sdk/service/access" "github.com/fatih/color" - "github.com/mattn/go-runewidth" "github.com/urfave/cli/v2" ) @@ -162,52 +159,8 @@ func promptForDatabaseAndUser(ctx context.Context, cfg *config.Context) (*access return nil, errors.New("you don't have access to any RDS databases") } - type Column struct { - Title string - Width int - } - cols := []Column{{Title: "Database", Width: 40}, {Title: "Role", Width: 40}} - var s = make([]string, 0, len(cols)) - for _, col := range cols { - style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) - renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) - s = append(s, lipgloss.NewStyle().Bold(true).Padding(0).Render(renderedCell)) - } - header := lipgloss.NewStyle().PaddingLeft(2).Render(lipgloss.JoinHorizontal(lipgloss.Left, s...)) - var options []huh.Option[*accessv1alpha1.Entitlement] - - for _, entitlement := range entitlements { - style := lipgloss.NewStyle().Width(cols[0].Width).MaxWidth(cols[0].Width).Inline(true) - target := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Target.Display(), cols[0].Width, "…"))) - - style = lipgloss.NewStyle().Width(cols[1].Width).MaxWidth(cols[1].Width).Inline(true) - role := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Role.Display(), cols[1].Width, "…"))) - - options = append(options, huh.Option[*accessv1alpha1.Entitlement]{ - Key: lipgloss.JoinHorizontal(lipgloss.Left, target, role), - Value: entitlement, - }) - } - - selector := huh.NewSelect[*accessv1alpha1.Entitlement](). - // show the filter dialog when there are 2 or more options - Filtering(len(options) > 1). - Options(options...). - Title("Select a database to connect to"). - Description(header).WithTheme(huh.ThemeBase()) - - err = selector.Run() - if err != nil { - return nil, err - } - - selectorVal := selector.GetValue() - - if selectorVal == nil { - return nil, errors.New("no database selected") - } + return proxy.PromptEntitlements(entitlements, "Database", "Role", "Select a database to connect to: ") - return selectorVal.(*accessv1alpha1.Entitlement), nil } func clientConnectionParameters(c *cli.Context, ensuredAccess *proxy.EnsureAccessOutput[*accessv1alpha1.AWSRDSOutput]) (connectionString, cliString string, port int, err error) {