From 97959ef5da19027db72566087c7c16f89cc969c0 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 15:30:43 +0100 Subject: [PATCH] refactor!: vertical render & UI/UX fixes See https://git.coopcloud.tech/coop-cloud/abra/pulls/454 --- cli/app/check.go | 9 ++- cli/app/deploy.go | 62 ++++++++++---------- cli/app/list.go | 26 ++------- cli/app/new.go | 102 +++++++++++++++----------------- cli/app/ps.go | 32 +++++++---- cli/app/secret.go | 9 ++- cli/app/services.go | 7 ++- cli/app/undeploy.go | 5 +- cli/app/volume.go | 9 +-- cli/internal/deploy.go | 107 +++++++++++++++++++++++++++------- cli/recipe/lint.go | 6 +- cli/recipe/list.go | 5 +- cli/recipe/version.go | 34 +++++++---- cli/server/list.go | 4 +- pkg/app/app.go | 2 +- pkg/formatter/formatter.go | 115 ++++++++++++++++++++++++++++++++----- pkg/recipe/git.go | 2 +- 17 files changed, 352 insertions(+), 184 deletions(-) diff --git a/cli/app/check.go b/cli/app/check.go index b48fb599..ac042f0a 100644 --- a/cli/app/check.go +++ b/cli/app/check.go @@ -46,7 +46,10 @@ ${FOO:} syntax). "check" does not confirm or deny this for you.`, } table. - Headers("RECIPE ENV SAMPLE", "APP ENV"). + Headers( + fmt.Sprintf("%s .env.sample", app.Recipe.Name), + fmt.Sprintf("%s.env", app.Name), + ). StyleFunc(func(row, col int) lipgloss.Style { switch { case col == 1: @@ -71,7 +74,9 @@ ${FOO:} syntax). "check" does not confirm or deny this for you.`, } } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } }, } diff --git a/cli/app/deploy.go b/cli/app/deploy.go index e9473c51..6841923d 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -110,19 +110,19 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, // is because we need to deal with GetComposeFiles under the hood and these // files change from version to version which therefore affects which // secrets might be generated - version := deployMeta.Version + toDeployVersion := deployMeta.Version if specificVersion != "" { - version = specificVersion - log.Debugf("choosing %s as version to deploy", version) + toDeployVersion = specificVersion + log.Debugf("choosing %s as version to deploy", toDeployVersion) var err error - isChaosCommit, err = app.Recipe.EnsureVersion(version) + isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion) if err != nil { log.Fatal(err) } if isChaosCommit { - log.Debugf("assuming '%s' is a chaos commit", version) + log.Debugf("assuming '%s' is a chaos commit", toDeployVersion) internal.Chaos = true } } @@ -138,12 +138,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, } } - if deployMeta.IsDeployed { - if internal.Force || internal.Chaos { - warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name)) - } else { - log.Fatalf("%s is already deployed", app.Name) - } + if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) { + log.Fatalf("%s is already deployed", app.Name) } if !internal.Chaos && specificVersion == "" { @@ -153,9 +149,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, } if len(versions) > 0 && !internal.Chaos { - version = versions[len(versions)-1] - log.Debugf("choosing %s as version to deploy", version) - if _, err := app.Recipe.EnsureVersion(version); err != nil { + toDeployVersion = versions[len(versions)-1] + log.Debugf("choosing %s as version to deploy", toDeployVersion) + if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil { log.Fatal(err) } } else { @@ -163,25 +159,22 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, if err != nil { log.Fatal(err) } - version = formatter.SmallSHA(head.String()) - warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit")) + toDeployVersion = formatter.SmallSHA(head.String()) } } - chaosVersion := config.CHAOS_DEFAULT + toDeployChaosVersion := config.CHAOS_DEFAULT if internal.Chaos { - warnMessages = append(warnMessages, "chaos mode engaged") - if isChaosCommit { - chaosVersion = specificVersion + toDeployChaosVersion = specificVersion versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() if err != nil { log.Fatal(err) } - version = versionLabelLocal + toDeployVersion = versionLabelLocal } else { var err error - chaosVersion, err = app.Recipe.ChaosVersion() + toDeployChaosVersion, err = app.Recipe.ChaosVersion() if err != nil { log.Fatal(err) } @@ -216,7 +209,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetChaosLabel(compose, stackName, internal.Chaos) - appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion) + appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion) appPkg.SetUpdateLabel(compose, stackName, app.Env) envVars, err := appPkg.CheckEnv(app) @@ -239,13 +232,24 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, log.Fatal(err) } } else { - warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app") + log.Debug("skipping domain checks as no DOMAIN=... configured for app") } } else { - warnMessages = append(warnMessages, "skipping domain checks as requested") + log.Debug("skipping domain checks as requested") } - if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil { + deployedVersion := "N/A" + if deployMeta.IsDeployed { + deployedVersion = deployMeta.Version + } + + if err := internal.DeployOverview( + app, + warnMessages, + deployedVersion, + deployMeta.ChaosVersion, + toDeployVersion, + toDeployChaosVersion); err != nil { log.Fatal(err) } @@ -267,9 +271,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, } } - app.Recipe.Version = version - if chaosVersion != config.CHAOS_DEFAULT { - app.Recipe.Version = chaosVersion + app.Recipe.Version = toDeployVersion + if toDeployChaosVersion != config.CHAOS_DEFAULT { + app.Recipe.Version = toDeployChaosVersion } log.Debugf("choosing %s as version to save to env file", app.Recipe.Version) if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { diff --git a/cli/app/list.go b/cli/app/list.go index d48c0dcb..be0753e7 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -208,7 +208,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, serverStat := allStats[app.Server] - headers := []string{"RECIPE", "DOMAIN"} + headers := []string{"RECIPE", "DOMAIN", "SERVER"} if status { headers = append(headers, []string{ "STATUS", @@ -228,7 +228,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, var rows [][]string for _, appStat := range serverStat.Apps { - row := []string{appStat.Recipe, appStat.Domain} + row := []string{appStat.Recipe, appStat.Domain, appStat.Server} if status { chaosStatus := appStat.Chaos if chaosStatus != "unknown" { @@ -256,20 +256,8 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, table.Rows(rows...) if len(rows) > 0 { - fmt.Println(table) - - if status { - fmt.Println(fmt.Sprintf( - "SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v", - app.Server, - serverStat.AppCount, - serverStat.VersionCount, - serverStat.UnversionedCount, - serverStat.LatestCount, - serverStat.UpgradeCount, - )) - } else { - log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) } if len(allStats) > 1 && len(rows) > 0 { @@ -279,12 +267,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, alreadySeen[app.Server] = true } - - if len(allStats) > 1 { - totalServers := formatter.BoldStyle.Render("TOTAL SERVERS") - totalApps := formatter.BoldStyle.Render("TOTAL APPS") - log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount) - } }, } diff --git a/cli/app/new.go b/cli/app/new.go index 8a0b53b1..4d9b7ddd 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -64,7 +64,6 @@ var AppNewCommand = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) - var recipeVersion string if !internal.Chaos { if err := recipe.EnsureIsClean(); err != nil { log.Fatal(err) @@ -74,41 +73,47 @@ var AppNewCommand = &cobra.Command{ log.Fatal(err) } } + } - if len(args) == 2 { - recipeVersion = args[1] - } + var recipeVersion string + if len(args) == 2 { + recipeVersion = args[1] + } - if recipeVersion == "" { - recipeVersions, err := recipe.GetRecipeVersions() - if err != nil { - log.Fatal(err) - } - - if len(recipeVersions) > 0 { - latest := recipeVersions[len(recipeVersions)-1] - for tag := range latest { - recipeVersion = tag - } - - if _, err := recipe.EnsureVersion(recipeVersion); err != nil { - log.Fatal(err) - } - } else { - if err := recipe.EnsureLatest(); err != nil { - log.Fatal(err) - } - } - } else { - if _, err := recipe.EnsureVersion(recipeVersion); err != nil { - log.Fatal(err) - } + var recipeVersions recipePkg.RecipeVersions + if recipeVersion == "" { + var err error + recipeVersions, err = recipe.GetRecipeVersions() + if err != nil { + log.Fatal(err) } } - if internal.Chaos && recipeVersion == "" { + if len(recipeVersions) > 0 { + latest := recipeVersions[len(recipeVersions)-1] + for tag := range latest { + recipeVersion = tag + } + + if _, err := recipe.EnsureVersion(recipeVersion); err != nil { + log.Fatal(err) + } + } else { + if err := recipe.EnsureLatest(); err != nil { + log.Fatal(err) + } + } + + if !internal.Chaos && recipeVersion != "" { + if _, err := recipe.EnsureVersion(recipeVersion); err != nil { + log.Fatal(err) + } + } + + chaosVersion := config.CHAOS_DEFAULT + if internal.Chaos { var err error - recipeVersion, err = recipe.ChaosVersion() + chaosVersion, err = recipe.ChaosVersion() if err != nil { log.Fatal(err) } @@ -187,37 +192,20 @@ var AppNewCommand = &cobra.Command{ newAppServer = "local" } - table, err := formatter.CreateTable() - if err != nil { - log.Fatal(err) - } - - headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"} - table.Headers(headers...) - - table.Row(newAppServer, appDomain, recipe.Name, recipeVersion) - - log.Infof("new app '%s' created 🌞", recipe.Name) - - fmt.Println("") - fmt.Println(table) - fmt.Println("") - - fmt.Println("Configure this app:") - fmt.Println(fmt.Sprintf("\n abra app config %s", appDomain)) - - fmt.Println("") - fmt.Println("Deploy this app:") - fmt.Println(fmt.Sprintf("\n abra app deploy %s", appDomain)) + log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion) if len(appSecrets) > 0 { - fmt.Println("") - fmt.Println("Generated secrets:") - fmt.Println("") - fmt.Println(secretsTable) + rows := [][]string{} + for k, v := range appSecrets { + rows = append(rows, []string{k, v}) + } + + overview := formatter.CreateOverview("SECRETS OVERVIEW", rows) + + fmt.Println(overview) log.Warnf( - "generated secrets %s shown again, please take note of them %s", + "secrets are %s shown again, please save them %s", formatter.BoldStyle.Render("NOT"), formatter.BoldStyle.Render("NOW"), ) diff --git a/cli/app/ps.go b/cli/app/ps.go index d818766c..a1ff5f33 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -128,24 +128,35 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao allContainerStats[containerStats["service"]] = containerStats + // NOTE(d1): don't clobber these variables for --machine output + dVersion := deployedVersion + cVersion := chaosVersion + + if containerStats["service"] != "app" { + // NOTE(d1): don't repeat info which only relevant for the "app" service + dVersion = "" + cVersion = "" + } + row := []string{ containerStats["service"], containerStats["image"], - containerStats["created"], + dVersion, + cVersion, containerStats["status"], - containerStats["state"], - containerStats["ports"], } rows = append(rows, row) } if internal.MachineReadable { - jsonstring, err := json.Marshal(allContainerStats) + rendered, err := json.Marshal(allContainerStats) if err != nil { log.Fatal("unable to convert to JSON: %s", err) } - fmt.Println(string(jsonstring)) + + fmt.Println(string(rendered)) + return } @@ -157,19 +168,18 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao headers := []string{ "SERVICE", "IMAGE", - "CREATED", + "VERSION", + "CHAOS", "STATUS", - "STATE", - "PORTS", } table. Headers(headers...). Rows(rows...) - fmt.Println(table) - - log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } } func init() { diff --git a/cli/app/secret.go b/cli/app/secret.go index 511135e7..5931c344 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -127,7 +127,9 @@ var AppSecretGenerateCommand = &cobra.Command{ return } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } log.Warnf( "generated secrets %s shown again, please take note of them %s", @@ -394,7 +396,10 @@ var AppSecretLsCommand = &cobra.Command{ return } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } + return } diff --git a/cli/app/services.go b/cli/app/services.go index 8e47beb2..b3313374 100644 --- a/cli/app/services.go +++ b/cli/app/services.go @@ -63,7 +63,7 @@ var AppServicesCommand = &cobra.Command{ log.Fatal(err) } - headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"} + headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"} table.Headers(headers...) var rows [][]string @@ -80,7 +80,6 @@ var AppServicesCommand = &cobra.Command{ row := []string{ serviceShortName, serviceLongName, - formatter.RemoveSha(container.Image), } rows = append(rows, row) @@ -89,7 +88,9 @@ var AppServicesCommand = &cobra.Command{ table.Rows(rows...) if len(rows) > 0 { - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } } }, } diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index 32709beb..e107e127 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -59,7 +59,10 @@ Passing "--prune/-p" does not remove those volumes.`, chaosVersion = deployMeta.ChaosVersion } - if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil { + if err := internal.UndeployOverview( + app, + deployMeta.Version, + chaosVersion); err != nil { log.Fatal(err) } diff --git a/cli/app/volume.go b/cli/app/volume.go index e7c64079..9813ba84 100644 --- a/cli/app/volume.go +++ b/cli/app/volume.go @@ -2,7 +2,6 @@ package app import ( "context" - "fmt" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" @@ -43,7 +42,7 @@ var AppVolumeListCommand = &cobra.Command{ log.Fatal(err) } - headers := []string{"name", "created", "mounted"} + headers := []string{"NAME", "ON SERVER"} table, err := formatter.CreateTable() if err != nil { @@ -54,14 +53,16 @@ var AppVolumeListCommand = &cobra.Command{ var rows [][]string for _, volume := range volumes { - row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint} + row := []string{volume.Name, volume.Mountpoint} rows = append(rows, row) } table.Rows(rows...) if len(rows) > 0 { - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } return } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 669c1053..d3fe5a29 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -6,6 +6,7 @@ import ( "strings" appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/lipgloss" @@ -20,7 +21,8 @@ var borderStyle = lipgloss.NewStyle(). var headerStyle = lipgloss.NewStyle(). Underline(true). - Bold(true) + Bold(true). + PaddingBottom(1) var leftStyle = lipgloss.NewStyle(). Bold(true) @@ -51,6 +53,11 @@ func NewVersionOverview( server = "local" } + domain := app.Domain + if domain == "" { + domain = "N/A" + } + body := strings.Builder{} body.WriteString( borderStyle.Render( @@ -60,7 +67,7 @@ func NewVersionOverview( lipgloss.JoinVertical( lipgloss.Left, horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), - horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), + horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(domain)), horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(currentVersion)), @@ -101,7 +108,13 @@ func NewVersionOverview( } // DeployOverview shows a deployment overview -func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error { +func DeployOverview( + app appPkg.App, + warnMessages []string, + deployedVersion string, + deployedChaosVersion string, + toDeployVersion, + toDeployChaosVersion string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") @@ -112,25 +125,25 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion server = "local" } - body := strings.Builder{} - body.WriteString( - borderStyle.Render( - lipgloss.JoinVertical( - lipgloss.Center, - headerStyle.Render("DEPLOY OVERVIEW"), - lipgloss.JoinVertical( - lipgloss.Left, - horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), - horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), - horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), - horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), - horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(version)), - horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Padding(0).Render(chaosVersion)), - ), - ), - ), - ) - fmt.Println(body.String()) + domain := app.Domain + if domain == "" { + domain = "N/A" + } + + rows := [][]string{ + []string{"APP", domain}, + []string{"RECIPE", app.Recipe.Name}, + []string{"SERVER", server}, + []string{"DEPLOYED", deployedVersion}, + []string{"CURRENT CHAOS ", deployedChaosVersion}, + []string{"TO DEPLOY", toDeployVersion}, + []string{"NEW CHAOS", toDeployChaosVersion}, + []string{"CONFIG", deployConfig}, + } + + overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) + + fmt.Println(overview) for _, msg := range warnMessages { log.Warn(msg) @@ -153,6 +166,56 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion return nil } +// UndeployOverview shows an undeployment overview +func UndeployOverview( + app appPkg.App, + version, + chaosVersion string) error { + deployConfig := "compose.yml" + if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { + deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + } + + server := app.Server + if app.Server == "default" { + server = "local" + } + + domain := app.Domain + if domain == "" { + domain = "N/A" + } + + rows := [][]string{ + []string{"APP", domain}, + []string{"RECIPE", app.Recipe.Name}, + []string{"SERVER", server}, + []string{"DEPLOYED", version}, + []string{"CHAOS", chaosVersion}, + []string{"CONFIG", deployConfig}, + } + + overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows) + + fmt.Println(overview) + + if NoInput { + return nil + } + + response := false + prompt := &survey.Confirm{Message: "proceed?"} + if err := survey.AskOne(prompt, &response); err != nil { + return err + } + + if !response { + log.Fatal("undeploy cancelled") + } + + return nil +} + // PostCmds parses a string of commands and executes them inside of the respective services // the commands string must have the following format: // " | |... " diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index 3cf7e8fb..030816de 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -1,8 +1,6 @@ package recipe import ( - "fmt" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" @@ -104,7 +102,9 @@ var RecipeLintCommand = &cobra.Command{ } if len(rows) > 0 { - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } for _, warnMsg := range warnMessages { log.Warn(warnMsg) diff --git a/cli/recipe/list.go b/cli/recipe/list.go index e7fce3d0..e0b44c08 100644 --- a/cli/recipe/list.go +++ b/cli/recipe/list.go @@ -79,8 +79,9 @@ var RecipeListCommand = &cobra.Command{ return } - fmt.Println(table) - log.Infof("total recipes: %v", len(rows)) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } } }, } diff --git a/cli/recipe/version.go b/cli/recipe/version.go index dc66f247..cc3dbc34 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -55,15 +55,32 @@ var RecipeVersionCommand = &cobra.Command{ log.Fatal(err) } - table.Headers("SERVICE", "NAME", "TAG") + table.Headers("SERVICE", "IMAGE", "TAG", "VERSION") for version, meta := range recipeMeta.Versions[i] { var allRows [][]string var rows [][]string for service, serviceMeta := range meta { - rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag}) - allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag}) + recipeVersion := version + if service != "app" { + recipeVersion = "" + } + + rows = append(rows, []string{ + service, + serviceMeta.Image, + serviceMeta.Tag, + recipeVersion, + }) + + allRows = append(allRows, []string{ + version, + service, + serviceMeta.Image, + serviceMeta.Tag, + recipeVersion, + }) } sort.Slice(rows, sortServiceByName(rows)) @@ -71,8 +88,9 @@ var RecipeVersionCommand = &cobra.Command{ table.Rows(rows...) if !internal.MachineReadable { - fmt.Println(table) - log.Infof("VERSION: %s", version) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } fmt.Println() continue } @@ -100,11 +118,7 @@ var RecipeVersionCommand = &cobra.Command{ func sortServiceByName(versions [][]string) func(i, j int) bool { return func(i, j int) bool { - // NOTE(d1): corresponds to the `tableCol` definition below - if versions[i][1] == "app" { - return true - } - return versions[i][1] < versions[j][1] + return versions[i][0] < versions[j][0] } } diff --git a/cli/server/list.go b/cli/server/list.go index 8eeb177a..dd3fd211 100644 --- a/cli/server/list.go +++ b/cli/server/list.go @@ -86,7 +86,9 @@ var ServerListCommand = &cobra.Command{ return } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } }, } diff --git a/pkg/app/app.go b/pkg/app/app.go index 25c00a99..a1083f8b 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -615,7 +615,7 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error { } if !skipped { - log.Infof("version %s saved to %s.env", version, a.Domain) + log.Debugf("version %s saved to %s.env", version, a.Domain) } else { log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain) } diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 56969513..a6279452 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -43,33 +43,122 @@ func HumanDuration(timestamp int64) string { // CreateTable prepares a table layout for output. func CreateTable() (*table.Table, error) { + var ( + renderer = lipgloss.NewRenderer(os.Stdout) + headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center) + cellStyle = renderer.NewStyle().Padding(0, 1) + borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + ) + table := table.New(). Border(lipgloss.ThickBorder()). - BorderStyle( - lipgloss.NewStyle(). - Foreground(lipgloss.Color("63")), - ) + BorderStyle(borderStyle). + StyleFunc(func(row, col int) lipgloss.Style { + var style lipgloss.Style + switch { + case row == table.HeaderRow: + return headerStyle + default: + style = cellStyle + } + + return style + }) + + return table, nil +} + +func PrintTable(t *table.Table) error { if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" { // NOTE(d1): no width limits for CI testing since we test against outputs log.Debug("detected ABRA_CI=1") - return table, nil + fmt.Println(t) + return nil } + tWidth, _ := lipgloss.Size(t.String()) + width, _, err := term.GetSize(0) if err != nil { - return nil, err + return err } - if width-10 < 79 { - // NOTE(d1): maintain standard minimum width - table.Width(79) - } else { - // NOTE(d1): tests show that this produces stable border drawing - table.Width(width - 10) + if tWidth > width { + t.Width(width - 10) } - return table, nil + fmt.Println(t) + + return nil +} + +// horizontal is a JoinHorizontal helper function. +func horizontal(left, mid, right string) string { + return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right) +} + +func CreateOverview(header string, rows [][]string) string { + var borderStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.ThickBorder()). + Padding(0, 1, 0, 1). + MaxWidth(79). + BorderForeground(lipgloss.Color("63")) + + var headerStyle = lipgloss.NewStyle(). + Underline(true). + Bold(true). + PaddingBottom(1) + + var leftStyle = lipgloss.NewStyle(). + Bold(true) + + var rightStyle = lipgloss.NewStyle() + + var longest int + for _, row := range rows { + if len(row[0]) > longest { + longest = len(row[0]) + } + } + + var renderedRows []string + for _, row := range rows { + if len(row) > 2 { + panic("CreateOverview: only accepts rows of len == 2") + } + + lenOffset := 4 + if len(row[0]) < longest { + lenOffset += longest - len(row[0]) + } + + offset := "" + for range lenOffset { + offset = offset + " " + } + + renderedRows = append( + renderedRows, + horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])), + ) + } + + body := strings.Builder{} + body.WriteString( + borderStyle.Render( + lipgloss.JoinVertical( + lipgloss.Center, + headerStyle.Render(header), + lipgloss.JoinVertical( + lipgloss.Left, + renderedRows..., + ), + ), + ), + ) + + return body.String() } // ToJSON converts a lipgloss.Table to JSON representation. It's not a robust diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go index 7a695fd3..8da23e8b 100644 --- a/pkg/recipe/git.go +++ b/pkg/recipe/git.go @@ -247,7 +247,7 @@ func (r Recipe) ChaosVersion() (string, error) { } if !isClean { - version = fmt.Sprintf("%s + unstaged changes", version) + version = fmt.Sprintf("%s+U", version) } return version, nil