Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* fix(addons): parse `maintenance-window-hour` as an int
* feat(session): remove region cache on logout
* feat: add JSON output for the `apps` command

## 1.44.1

Expand Down
42 changes: 18 additions & 24 deletions apps/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ package apps

import (
"context"
"fmt"
"os"

"github.com/olekukonko/tablewriter"

"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
"github.com/Scalingo/go-scalingo/v11"
"github.com/Scalingo/go-utils/errors/v3"
)

func List(ctx context.Context, projectSlug string) error {
func List(ctx context.Context, renderer renderer.Renderer[[]*scalingo.App], projectSlug string) error {
c, err := config.ScalingoClient(ctx)
if err != nil {
return errors.Wrap(ctx, err, "get Scalingo client")
Expand All @@ -24,30 +20,28 @@ func List(ctx context.Context, projectSlug string) error {
return errors.Wrap(ctx, err, "list apps")
}

if len(apps) == 0 {
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
return nil
filteredApps := filterAppsByProject(apps, projectSlug)
renderer.SetData(ctx, filteredApps)

err = renderer.Render(ctx)
if err != nil {
return errors.Wrap(ctx, err, "render apps list")
}

t := tablewriter.NewWriter(os.Stdout)
t.Header([]string{"Name", "Role", "Status", "Project"})
return nil
}

currentUser, err := config.C.CurrentUser(ctx)
if err != nil {
return errors.Wrap(ctx, err, "fail to get current user")
func filterAppsByProject(apps []*scalingo.App, projectSlug string) []*scalingo.App {
if projectSlug == "" {
return apps
}

filteredApps := make([]*scalingo.App, 0, len(apps))
for _, app := range apps {
// If a filter was set but the app is not in the project, skip to the next one.
if projectSlug != "" && projectSlug != app.ProjectSlug() {
continue
if app.ProjectSlug() == projectSlug {
filteredApps = append(filteredApps, app)
}

role := utils.AppRole(currentUser, app)

_ = t.Append([]string{app.Name, string(role), string(app.Status), app.ProjectSlug()})
}
_ = t.Render()

return nil
return filteredApps
}
31 changes: 28 additions & 3 deletions cmd/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import (

"github.com/Scalingo/cli/apps"
"github.com/Scalingo/cli/cmd/autocomplete"
"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/detect"
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
rendererjson "github.com/Scalingo/cli/internal/boundaries/out/renderer/json"
renderertable "github.com/Scalingo/cli/internal/boundaries/out/renderer/table"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/go-scalingo/v11"
"github.com/Scalingo/go-utils/errors/v3"
)

Expand All @@ -19,17 +24,37 @@ var (
Name: "apps",
Category: "Global",
Description: "List your apps and give some details about them",
Flags: []cli.Flag{&cli.StringFlag{Name: "project", Usage: "Filter apps by project. The filter uses the format <ownerUsername>/<projectName>"}},
Usage: "List your apps",
Flags: []cli.Flag{
&cli.StringFlag{Name: "project", Usage: "Filter apps by project. The filter uses the format <ownerUsername>/<projectName>"},
},
Usage: "List your apps",
Action: func(ctx context.Context, c *cli.Command) error {
projectSlug := c.String("project")
format := renderer.Format(c.String("format"))

if projectSlug != "" {
projectSlugSplit := strings.Split(projectSlug, "/")
if len(projectSlugSplit) != 2 || (len(projectSlugSplit) == 2 && (projectSlugSplit[0] == "" || projectSlugSplit[1] == "")) {
errorQuitWithHelpMessage(ctx, errors.New(ctx, "project filter doesn't respect the expected format"), c, "apps")
}
}
err := apps.List(ctx, projectSlug)

var appsRenderer renderer.Renderer[[]*scalingo.App]
switch format {
case renderer.FormatTable:
currentUser, err := config.C.CurrentUser(ctx)
if err != nil {
errorQuit(ctx, errors.Wrap(ctx, err, "get current user"))
}

appsRenderer = renderertable.NewAppsList(currentUser)
case renderer.FormatJSON:
appsRenderer = rendererjson.NewAppsList()
default:
errorQuitWithHelpMessage(ctx, errors.Newf(ctx, "invalid format '%v'", format), c, "apps")
}

err := apps.List(ctx, appsRenderer, projectSlug)
if err != nil {
errorQuit(ctx, err)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/boundaries/out/renderer/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package renderer

type Format string

const (
FormatJSON Format = "json"
FormatTable Format = "table"
)
35 changes: 35 additions & 0 deletions internal/boundaries/out/renderer/json/apps_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package json

import (
"context"
"encoding/json"
"os"

"github.com/Scalingo/cli/internal/boundaries/out/renderer"
"github.com/Scalingo/go-scalingo/v11"
"github.com/Scalingo/go-utils/errors/v3"
)

type appsListRenderer struct {
apps []*scalingo.App
}

type appsListResponse struct {
Apps []*scalingo.App `json:"apps"`
}

func NewAppsList() renderer.Renderer[[]*scalingo.App] {
return &appsListRenderer{}
}

func (r *appsListRenderer) Render(ctx context.Context) error {
err := json.NewEncoder(os.Stdout).Encode(appsListResponse{Apps: r.apps})
if err != nil {
return errors.Wrap(ctx, err, "encode apps list to JSON")
}
return nil
}

func (r *appsListRenderer) SetData(ctx context.Context, apps []*scalingo.App) {
r.apps = apps
}
8 changes: 8 additions & 0 deletions internal/boundaries/out/renderer/renderer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package renderer

import "context"

type Renderer[D any] interface {
Render(ctx context.Context) error
SetData(ctx context.Context, data D)
}
50 changes: 50 additions & 0 deletions internal/boundaries/out/renderer/table/apps_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package table

import (
"context"
"fmt"
"os"

"github.com/olekukonko/tablewriter"

"github.com/Scalingo/cli/internal/boundaries/out/renderer"
"github.com/Scalingo/cli/io"
"github.com/Scalingo/cli/utils"
"github.com/Scalingo/go-scalingo/v11"
"github.com/Scalingo/go-utils/errors/v3"
)

type appsListRenderer struct {
currentUser *scalingo.User
apps []*scalingo.App
}

func NewAppsList(currentUser *scalingo.User) renderer.Renderer[[]*scalingo.App] {
return &appsListRenderer{
currentUser: currentUser,
}
}

func (r *appsListRenderer) Render(ctx context.Context) error {
if len(r.apps) == 0 {
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: When filtering apps by project, is this message still valid?

Is it possible the user may have created apps that are not in the project?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmmh that's a good point 🤔

Would the following phrasing be better for you?

Suggested change
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
fmt.Println(io.Indent("\nYou haven't created any app yet in this project, create your first application using:\n→ scalingo create <app_name>\n", 2))

Or you thought about something else?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works.

return nil
}

t := tablewriter.NewWriter(os.Stdout)
t.Header([]string{"Name", "Role", "Status", "Project"})

for _, app := range r.apps {
role := utils.AppRole(r.currentUser, app)
err := t.Append([]string{app.Name, string(role), string(app.Status), app.ProjectSlug()})
if err != nil {
return errors.Wrap(ctx, err, "append app to table")
}
}

return t.Render()
}

func (r *appsListRenderer) SetData(ctx context.Context, apps []*scalingo.App) {
r.apps = apps
}
2 changes: 2 additions & 0 deletions scalingo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/Scalingo/cli/cmd"
"github.com/Scalingo/cli/cmd/autocomplete"
"github.com/Scalingo/cli/config"
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
"github.com/Scalingo/cli/signals"
"github.com/Scalingo/cli/update"
"github.com/Scalingo/go-scalingo/v11/debug"
Expand Down Expand Up @@ -88,6 +89,7 @@ func main() {
app.Flags = []cli.Flag{
&cli.StringFlag{Name: "addon", Value: "<addon_id>", Usage: "ID of the current addon", Sources: cli.EnvVars("SCALINGO_ADDON")},
&cli.StringFlag{Name: "app", Aliases: []string{"a"}, Value: "<name>", Usage: "Name of the app", Sources: cli.EnvVars("SCALINGO_APP")},
&cli.StringFlag{Name: "format", Value: string(renderer.FormatTable), Usage: "[" + string(renderer.FormatJSON) + "|" + string(renderer.FormatTable) + "]"},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: will this root --format flag conflict with other exiting --format flags?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! I double checked and I confirm that the format command declared for the private-networks-domain-names command keeps working

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks nice.

&cli.StringFlag{Name: "remote", Aliases: []string{"r"}, Value: "scalingo", Usage: "Name of the remote"},
&cli.StringFlag{Name: "region", Value: "", Usage: "Name of the region to use"},
}
Expand Down