Skip to content
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

Interactive namespace selection #1285

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
11ac703
Add support for typescript in the nodejs runtime (#1225)
joshuaauerbachwatson Sep 7, 2022
a75e480
Eliminate plugin usage for sls fn invoke (#1226)
joshuaauerbachwatson Sep 12, 2022
fb0ffea
Merge branch 'main' into reverse-merge-main
joshuaauerbachwatson Sep 20, 2022
a8e75d1
Merge pull request #1239 from joshuaauerbachwatson/reverse-merge-main
joshuaauerbachwatson Sep 20, 2022
908c9ce
Add doctl serverless trigger support (for scheduled functions) (#1232)
joshuaauerbachwatson Sep 27, 2022
22fc885
Merge branch 'main' into latest-main
joshuaauerbachwatson Sep 27, 2022
083868e
Merge pull request #1253 from joshuaauerbachwatson/latest-main
joshuaauerbachwatson Sep 27, 2022
068350a
Merge branch 'main' into feature/serverless
andrewsomething Sep 27, 2022
7ca75fc
Eliminate plugin usage in 'doctl sls fn list'
joshuaauerbachwatson Sep 8, 2022
0c5d710
Hidden flags enabling connection to dev clusters
joshuaauerbachwatson Sep 12, 2022
c5a2361
Fix unit test (date handling is timezone specific)
joshuaauerbachwatson Sep 26, 2022
343ac51
Eliminate call to auth/current via the plugin
joshuaauerbachwatson Sep 26, 2022
8a26142
Commit changed test (screwed it up last time)
joshuaauerbachwatson Sep 26, 2022
407f28a
Remove accidental re-introduction of --beta flag
joshuaauerbachwatson Sep 28, 2022
bd979f2
Bump min nim version to incorporate small bug fix
joshuaauerbachwatson Sep 29, 2022
f2687c4
Merge pull request #1252 from joshuaauerbachwatson/function-list-no-p…
joshuaauerbachwatson Sep 29, 2022
64028da
Avoid plugin in serverless activations [ get | result ] (#1270)
joshuaauerbachwatson Oct 11, 2022
5b22684
Updates activation list command to use the whisk client instead of th…
ddebarros Oct 11, 2022
142fe8d
Adds name filter to activations list
ddebarros Oct 11, 2022
b51dccc
moved getActivationStatus to utils
ddebarros Oct 11, 2022
f3e3ebd
re-generates mocks
ddebarros Oct 11, 2022
046ee83
made changes from PR review and updated the unit tests
ddebarros Oct 12, 2022
72bfb84
Updates test
ddebarros Oct 12, 2022
37f36ad
default to json output when the full flag is set
ddebarros Oct 12, 2022
ecd13f1
list command takes a function name not activation name
ddebarros Oct 12, 2022
a04b853
Updates the apache-go-client library and updates the tests to reflect…
ddebarros Oct 14, 2022
d4abffc
Makes updates from PR review
ddebarros Oct 14, 2022
ca6267a
Merge pull request #1277 from ddebarros/add-activations-list
ddebarros Oct 14, 2022
caa607b
Uses bubbles list to display an interactive namespace selection list
ddebarros Oct 17, 2022
2b761a8
Uses bubbles list to display an interactive namespace selection list
ddebarros Oct 17, 2022
88f1753
Adds a new spinner re-usable component
ddebarros Oct 18, 2022
5acfef5
Updates tests
ddebarros Oct 18, 2022
056c480
makes spinner message private
ddebarros Oct 20, 2022
e98354d
Updates command to respect the interactive flag
ddebarros Oct 20, 2022
d25febf
Merge remote-tracking branch 'origin' into interactive-namespace-sele…
ddebarros Oct 27, 2022
f8ac4ca
Merge branch 'main' of https://github.com/digitalocean/doctl into int…
ddebarros Nov 1, 2022
9a9c47d
Updates interactive check
ddebarros Nov 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion commands/charm/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func (l *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return l, cmd
}

// Update implements bubbletea.Model.
// View implements bubbletea.Model.
func (l *listModel) View() string {
return l.style.Lipgloss().Render(l.model.View())
}
Expand Down
95 changes: 95 additions & 0 deletions commands/charm/spinner/spinner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package spinner

import (
"fmt"
"os"

s "github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
)

type SpinningLoader struct {
model s.Model
prog *tea.Program
cancel bool
message string
}

type Option func(*SpinningLoader)

// New creates a new spinning loader.
func New(message string, opts ...Option) SpinningLoader {
sm := s.New()
sm.Spinner = s.Dot

l := SpinningLoader{
model: sm,
message: message,
}

for _, opt := range opts {
opt(&l)
}
return l
}

// New creates a new spinner.
func (sl *SpinningLoader) Start() error {
p := tea.NewProgram((*SpinningLoader)(sl))
sl.prog = p

if err := p.Start(); err != nil {
return err
}

if sl.cancel {
os.Exit(1)
}
return nil
}

func (sl *SpinningLoader) Stop() {
if sl.prog != nil {
sl.prog.Kill()
}
}

// Init implements bubbletea.Model.
func (sl *SpinningLoader) Init() tea.Cmd {
return sl.model.Tick
}

// Update implements bubbletea.Model.
func (sl *SpinningLoader) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case tea.KeyCtrlC.String():
sl.cancel = true
return sl, tea.Quit
}

case s.TickMsg:
var cmd tea.Cmd
sl.model, cmd = sl.model.Update(msg)
return sl, cmd
}

return sl, nil
}

// View implements bubbletea.Model.
func (sl *SpinningLoader) View() string {
return fmt.Sprintf("%s %s", sl.model.View(), sl.message)
}

// Model returns the underlying SpinningLoader.model
func (sl SpinningLoader) Model() *s.Model {
return &sl.model
}

func WithSpinner(s s.Spinner) Option {
return func(sl *SpinningLoader) {
sl.model.Spinner = s
}
}
3 changes: 3 additions & 0 deletions commands/doit.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,5 +307,8 @@ func cmdNS(cmd *cobra.Command) string {
}

func isTerminal(f *os.File) bool {
if os.Getenv("TERM") == "dumb" {
return false
}
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}
15 changes: 15 additions & 0 deletions commands/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"strings"

"github.com/digitalocean/doctl"

"github.com/digitalocean/doctl/commands/charm/spinner"
"github.com/digitalocean/doctl/commands/displayers"
"github.com/digitalocean/doctl/do"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -226,14 +228,27 @@ func getValidRegion(value string) string {
// get the Namespaces that match a pattern, where the "pattern" has no wildcards but can be a
// prefix, infix, or suffix match to a namespace ID or label.
func getMatchingNamespaces(ctx context.Context, ss do.ServerlessService, pattern string) ([]do.OutputNamespace, error) {
var loader spinner.SpinningLoader
if Interactive {
loader = spinner.New("Loading namespaces ...")
go loader.Start()
}

ans := []do.OutputNamespace{}
list, err := ss.ListNamespaces(ctx)

if Interactive {
loader.Stop()
}

if err != nil {
return ans, err
}

if pattern == "" {
return list.Namespaces, nil
}

for _, ns := range list.Namespaces {
if strings.Contains(ns.Namespace, pattern) || strings.Contains(ns.Label, pattern) {
ans = append(ans, ns)
Expand Down
66 changes: 28 additions & 38 deletions commands/serverless.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ limitations under the License.
package commands

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"

"github.com/digitalocean/doctl"
"github.com/digitalocean/doctl/commands/charm/list"
"github.com/digitalocean/doctl/commands/charm/template"
"github.com/digitalocean/doctl/do"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -264,49 +263,40 @@ func RunServerlessConnect(c *CmdConfig) error {
// connectFromList connects a namespace based on a non-empty list of namespaces. If the list is
// singular that determines the namespace that will be connected. Otherwise, this is determined
// via a prompt.
func connectFromList(ctx context.Context, sls do.ServerlessService, list []do.OutputNamespace, out io.Writer) error {
var ns do.OutputNamespace
if len(list) == 1 {
ns = list[0]
} else {
ns = chooseFromList(list, out)
if ns.Namespace == "" {
return nil
func connectFromList(ctx context.Context, sls do.ServerlessService, l []do.OutputNamespace, out io.Writer) error {
if len(l) == 1 {
creds, err := sls.GetNamespace(ctx, l[0].Namespace)
if err != nil {
return err
}
return finishConnecting(sls, creds, l[0].Label, out)
}
creds, err := sls.GetNamespace(ctx, ns.Namespace)
if err != nil {
return err

if !Interactive {
return errors.New("Namespace is required when running non-interactively")
}
return finishConnecting(sls, creds, ns.Label, out)
}

// connectChoiceReader is the bufio.Reader for reading the user's response to the prompt to choose
// a namespace. It can be replaced for testing.
var connectChoiceReader *bufio.Reader = bufio.NewReader(os.Stdin)
var nsItems []list.Item

// chooseFromList displays a list of namespaces (label, region, id) assigning each one a number.
// The user can than respond to a prompt that chooses from the list by number. The response 'x' is
// also accepted and exits the command.
func chooseFromList(list []do.OutputNamespace, out io.Writer) do.OutputNamespace {
for i, ns := range list {
fmt.Fprintf(out, "%d: %s in %s, label=%s\n", i, ns.Namespace, ns.Region, ns.Label)
for _, ns := range l {
nsItems = append(nsItems, nsListItem{ns: ns})
}
for {
fmt.Fprintln(out, "Choose a namespace by number or 'x' to exit")
choice, err := connectChoiceReader.ReadString('\n')
if err != nil {
continue
}
choice = strings.TrimSpace(choice)
if choice == "x" {
return do.OutputNamespace{}
}
i, err := strconv.Atoi(choice)
if err == nil && i >= 0 && i < len(list) {
return list[i]
}

listItems := list.New(nsItems)
listItems.Model().Title = "select a namespace"
listItems.Model().SetStatusBarItemName("namespace", "namespaces")

selected, err := listItems.Select()
if err != nil {
return err
}

selectedNs := selected.(nsListItem).ns
creds, err := sls.GetNamespace(ctx, selectedNs.Namespace)
if err != nil {
return err
}
return finishConnecting(sls, creds, selectedNs.Label, out)
}

// finishConnecting performs the final steps of 'doctl serverless connect'.
Expand Down
21 changes: 21 additions & 0 deletions commands/serverless_charm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package commands

import (
"github.com/digitalocean/doctl/do"
)

type nsListItem struct {
ns do.OutputNamespace
}

func (i nsListItem) Title() string {
return i.ns.Label + " (" + i.ns.Region + ")"
}

func (i nsListItem) Description() string {
return i.ns.Namespace
}

func (i nsListItem) FilterValue() string {
return i.ns.Label
}
4 changes: 1 addition & 3 deletions commands/serverless_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ limitations under the License.
package commands

import (
"bufio"
"bytes"
"context"
"errors"
Expand Down Expand Up @@ -68,7 +67,7 @@ func TestServerlessConnect(t *testing.T) {
Label: "another",
},
},
expectedOutput: "0: ns1 in nyc1, label=something\n1: ns2 in lon1, label=another\nChoose a namespace by number or 'x' to exit\nConnected to functions namespace 'ns1' on API host 'https://api.example.com' (label=something)\n\n",
expectedError: errors.New("Namespace is required when running non-interactively"),
},
{
name: "use argument",
Expand Down Expand Up @@ -96,7 +95,6 @@ func TestServerlessConnect(t *testing.T) {
if tt.doctlArg != "" {
config.Args = append(config.Args, tt.doctlArg)
}
connectChoiceReader = bufio.NewReader(strings.NewReader("0\n"))
nsResponse := do.NamespaceListResponse{Namespaces: tt.namespaceList}
creds := do.ServerlessCredentials{Namespace: "ns1", APIHost: "https://api.example.com"}

Expand Down