diff --git a/README.md b/README.md index c6cbfac..f873620 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,14 @@ $ e1s -version | `k`, `↑` | Select previous item | | `l`, `←`, `Enter` | Enter current resource/SSH | | `h`, `→`, `Esc` | Go to previous view | -| `d` | Describe selected resource | +| `d` | Describe selected resource(show json) | | `t` | Describe task definition | | `w` | Describe service events | | `a` | Show service auto scaling | | `m` | Show service metrics(CPUUtilization/MemoryUtilization) | -| `r` | List task definition revisions | +| `r` | Reload resources | +| `v` | List task definition revisions | +| `f` | Toggle full screen | | `e` | Edit resource | | `b` | Open selected resource in AWS web console | | `ctrl` + `c` | Quit | diff --git a/api/auto_scaling.go b/api/auto_scaling.go index 76e2008..0a84f6e 100644 --- a/api/auto_scaling.go +++ b/api/auto_scaling.go @@ -121,7 +121,7 @@ func (store *Store) describeScheduledAction(serviceArn *string) ([]types.Schedul actionsOutput, err := store.autoScaling.DescribeScheduledActions(context.Background(), actionsInput) if err != nil { - logger.Printf("aws failed to auto scaling scheduled actions serviceArn: \"%s\", err: %v\n", *serviceArn, err) + logger.Printf("e1s - aws failed to auto scaling scheduled actions serviceArn: \"%s\", err: %v\n", *serviceArn, err) return nil, err } diff --git a/api/cluster.go b/api/cluster.go index f8116d4..bffc282 100644 --- a/api/cluster.go +++ b/api/cluster.go @@ -26,7 +26,7 @@ type Store struct { func NewStore() *Store { cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(os.Getenv("AWS_REGION"))) if err != nil { - logger.Printf("unable to load SDK config, error: %v\n", err) + logger.Printf("e1s - aws unable to load SDK config, error: %v\n", err) } ecsClient := ecs.NewFromConfig(cfg) return &Store{ diff --git a/main.go b/main.go index ec39706..80f4672 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,16 @@ package main import ( "fmt" + "os" "github.com/keidarcy/e1s/ui" + "github.com/keidarcy/e1s/util" ) func main() { if err := ui.Show(); err != nil { - fmt.Println("e1s failed to start, valid aws cli and aws cli profile are required") - panic(err) + util.Logger.Printf("e1s - failed to start, error: %v\n", err) + fmt.Println("e1s failed to start, please check your aws cli credential.") + os.Exit(1) } } diff --git a/ui/cluster.go b/ui/cluster.go index 0c8a707..a1bc6f1 100644 --- a/ui/cluster.go +++ b/ui/cluster.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "os" "strconv" "strings" @@ -25,14 +26,17 @@ func newClusterView(clusters []types.Cluster, app *App) *ClusterView { func (app *App) showClustersPage() error { clusters, err := app.Store.ListClusters() if err != nil { - logger.Printf("show clusters failed, error: %v\n", err) + logger.Printf("e1s - show clusters failed, error: %v\n", err) return err } view := newClusterView(clusters, app) + if len(clusters) == 0 { - go view.flashModal(fmt.Sprintf(initErrorFmt, app.Region), 10) + fmt.Printf("There is no valid clusters in \033[31m%s\033[0m. Please check you ecs cluster via `AWS_REGION=%s aws ecs list-clusters`.\n", app.Region, app.Region) + os.Exit(0) } + page := buildAppPage(view) view.addAppPage(page) return nil diff --git a/ui/components.go b/ui/components.go index 812c820..100c2fd 100644 --- a/ui/components.go +++ b/ui/components.go @@ -46,19 +46,21 @@ func (v *View) styledForm(title string) *tview.Form { return f } -func (v *View) errorModal(text string) { - v.flashModal(fmt.Sprintf("[red::b]%s ", text), 3) +// Call this function need a new goroutine +func (v *View) errorModal(text string, duration, width, height int) { + v.flashModal(fmt.Sprintf("[red::b]%s ", text), duration, width, height) } -func (v *View) successModal(text string) { - v.flashModal(fmt.Sprintf("[green::b]%s ", text), 3) +// Call this function need a new goroutine +func (v *View) successModal(text string, duration, width, height int) { + v.flashModal(fmt.Sprintf("[green::b]%s ", text), duration, width, height) } // show a flash modal in a given time duration -func (v *View) flashModal(text string, duration int) { +func (v *View) flashModal(text string, duration, width, height int) { t := tview.NewTextView().SetDynamicColors(true).SetText(text) t.SetBorder(true) - v.app.Pages.AddPage(text, v.modal(t, 100, 10), true, true) + v.app.Pages.AddPage(text, v.modal(t, width, height), true, true) t.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { v.closeModal() return event diff --git a/ui/container.go b/ui/container.go index be2c7e7..b21ac85 100644 --- a/ui/container.go +++ b/ui/container.go @@ -17,7 +17,7 @@ import ( const ( shell = "/bin/sh" awsCli = "aws" - sshBannerFmt = "\033[1;31m\033[46m <>: \033[0m Cluster: \"%s\" | Service: \"%s\" | Task: \"%s\" | Container: \"%s\"" + sshBannerFmt = "\033[1;31m<>\033[0m: \n#######################################\n\033[1;32mCluster\033[0m: \"%s\" \n\033[1;32mService\033[0m: \"%s\" \n\033[1;32mTask\033[0m: \"%s\" \n\033[1;32mContainer\033[0m: \"%s\"\n#######################################\n" ) type ContainerView struct { @@ -89,31 +89,39 @@ func (v *ContainerView) tableHandler() { v.ssh(containerName) }) - v.table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - // simulate selected action(ssh) - sshHandler := func() { - selected := v.getCurrentSelection() - containerName := *selected.container.Name - v.ssh(containerName) - } - - // handle right arrow key - if event.Key() == tcell.KeyRight { - sshHandler() - return event - } + v.table.SetInputCapture(v.handleInputCapture) +} - // handle l key - key := event.Rune() - switch key { - case lKey, lKey - upperLowerDiff: +// Container page specific input handler +func (v *ContainerView) handleInputCapture(event *tcell.EventKey) *tcell.EventKey { + // simulate selected action(ssh) + sshHandler := func() { + selected := v.getCurrentSelection() + containerName := *selected.container.Name + v.ssh(containerName) + } - sshHandler() - case hKey, hKey - upperLowerDiff: - v.handleDone(0) - } + // handle right arrow key + if event.Key() == tcell.KeyRight { + sshHandler() return event - }) + } + + // handle l key + key := event.Rune() + switch key { + case rKey, rKey - upperLowerDiff: + v.reloadResource() + case lKey, lKey - upperLowerDiff: + sshHandler() + case hKey, hKey - upperLowerDiff: + v.handleDone(0) + case bKey, bKey - upperLowerDiff: + v.openInBrowser() + case dKey, dKey - upperLowerDiff: + v.switchToResourceJson() + } + return event } // Generate info pages params @@ -184,7 +192,7 @@ func (v *ContainerView) ssh(containerName string) { } bin, err := exec.LookPath(awsCli) if err != nil { - logger.Printf("aws binary not found, error: %v\n", err) + logger.Printf("e1s - aws cli binary not found, error: %v\n", err) v.back() } arg := []string{ diff --git a/ui/json.go b/ui/json.go index fa103e9..7c9b63e 100644 --- a/ui/json.go +++ b/ui/json.go @@ -210,7 +210,7 @@ func (v *View) getJsonString(entity Entity, which string) string { jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { - logger.Printf("json page marshal indent failed, error: %v\n", err) + logger.Printf("e1s - json page marshal indent failed, error: %v\n", err) return "json page marshal indent failed" } diff --git a/ui/modal.go b/ui/modal.go index 8e5f793..5c59667 100644 --- a/ui/modal.go +++ b/ui/modal.go @@ -217,7 +217,7 @@ func (v *View) serviceUpdateContent() (*tview.Form, string) { // get data for form families, err := v.app.Store.ListTaskDefinitionFamilies() if err != nil { - v.errorModal("aws api error!") + v.errorModal("aws api error!", 2, 20, 10) v.closeModal() } @@ -247,7 +247,7 @@ func (v *View) serviceUpdateContent() (*tview.Form, string) { // when family option change, change revision drop down value taskDefinitions, err := v.app.Store.ListTaskDefinition(&text) if err != nil { - v.errorModal("aws api error!") + v.errorModal("aws api error!", 2, 20, 10) v.closeModal() } revisions := []string{} @@ -315,10 +315,12 @@ func (v *View) serviceUpdateContent() (*tview.Form, string) { if err != nil { v.closeModal() - go v.errorModal(err.Error()) + go v.errorModal(err.Error(), 5, 100, 10) + v.reloadResource() } else { v.closeModal() - go v.successModal(fmt.Sprintf("SUCCESS 🚀\nDesiredCount: %d\nTaskDefinition: %s\n", s.DesiredCount, *s.TaskDefinition)) + go v.successModal(fmt.Sprintf("SUCCESS 🚀\nDesiredCount: %d\nTaskDefinition: %s\n", s.DesiredCount, *s.TaskDefinition), 5, 110, 5) + v.reloadResource() } }) return f, title diff --git a/ui/service.go b/ui/service.go index b6ce2d3..61d600d 100644 --- a/ui/service.go +++ b/ui/service.go @@ -34,7 +34,7 @@ func newServiceView(services []types.Service, app *App) *ServiceView { func (app *App) showServicesPage() error { services, err := app.Store.ListServices(app.cluster.ClusterName) if err != nil { - logger.Printf("show services page failed, error: %v\n", err) + logger.Printf("e1s - show services page failed, error: %v\n", err) return err } diff --git a/ui/table.go b/ui/table.go index e63a5eb..6c29e6c 100644 --- a/ui/table.go +++ b/ui/table.go @@ -81,7 +81,7 @@ func (v *View) handleSelectionChanged(row, column int) { func (v *View) handleSelected(row, column int) { err := v.handleAppPageSwitch(v.app.entityName, false) if err != nil { - logger.Printf("page change failed, error: %v\n", err) + logger.Printf("e1s - page change failed, error: %v\n", err) v.back() } } @@ -109,6 +109,8 @@ func (v *View) handleInputCapture(event *tcell.EventKey) *tcell.EventKey { case tKey, tKey - upperLowerDiff: v.switchToTaskDefinitionJson() case rKey, rKey - upperLowerDiff: + v.reloadResource() + case vKey, vKey - upperLowerDiff: v.switchToTaskDefinitionRevisionsJson() case wKey, wKey - upperLowerDiff: v.switchToServiceEventsJson() @@ -180,7 +182,7 @@ func (v *View) openInBrowser() { logger.Printf("open url: %s\n", url) err := util.OpenURL(url) if err != nil { - logger.Printf("failed open url %s\n", url) + logger.Printf("e1s - failed open url %s\n", url) } } @@ -195,7 +197,7 @@ func (v *View) editTaskDefinition() { taskDefinition := *selected.task.TaskDefinitionArn td, err := v.app.Store.DescribeTaskDefinition(&taskDefinition) if err != nil { - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } names := strings.Split(selected.entityName, "/") @@ -204,7 +206,7 @@ func (v *View) editTaskDefinition() { tmpfile, err := os.CreateTemp("", names[len(names)-1]) if err != nil { logger.Println("Error creating temporary file:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } defer os.Remove(tmpfile.Name()) @@ -213,13 +215,13 @@ func (v *View) editTaskDefinition() { originalTD, err := json.MarshalIndent(td, "", " ") if err != nil { logger.Println("Error reading temporary file:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } if _, err := tmpfile.Write(originalTD); err != nil { logger.Println("Error writing to temporary file:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } @@ -236,14 +238,14 @@ func (v *View) editTaskDefinition() { if err := cmd.Run(); err != nil { logger.Println("Error opening editor:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } editedTD, err := os.ReadFile(tmpfile.Name()) if err != nil { logger.Println("Error reading temporary file:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } @@ -254,14 +256,14 @@ func (v *View) editTaskDefinition() { // if no change do nothing if bytes.Equal(originalTD, editedTD) { - v.flashModal(" no change", 2) + v.flashModal(" Task definition has no change.", 2, 50, 3) return } var updatedTd ecs.RegisterTaskDefinitionInput if err := json.Unmarshal(editedTD, &updatedTd); err != nil { logger.Println("Error unmarshaling JSON:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } @@ -270,10 +272,10 @@ func (v *View) editTaskDefinition() { if err != nil { logger.Println("Error opening editor:", err) - v.errorModal(errMsg) + v.errorModal(errMsg, 2, 110, 10) return } - v.successModal(fmt.Sprintf("SUCCESS 🚀\nTaskDefinition Family: %s\nRevision: %d\n", family, revision)) + v.successModal(fmt.Sprintf("SUCCESS 🚀\nTaskDefinition Family: %s\nRevision: %d\n", family, revision), 5, 110, 5) } v.showTaskDefinitionConfirm(register) diff --git a/ui/task.go b/ui/task.go index 43e7a7a..2e76a48 100644 --- a/ui/task.go +++ b/ui/task.go @@ -31,7 +31,7 @@ func (app *App) showTasksPages() error { tasks, err := app.Store.ListTasks(app.cluster.ClusterName, app.service.ServiceName) if err != nil { - logger.Printf("show tasks pages failed, error: %v\n", err) + logger.Printf("e1s - show tasks pages failed, error: %v\n", err) return err } diff --git a/ui/view.go b/ui/view.go index ebd5112..5834659 100644 --- a/ui/view.go +++ b/ui/view.go @@ -19,7 +19,6 @@ const ( clusterTasksFmt = "[blue]%d pending[-] | [green]%d running" serviceTasksFmt = "%d/%d tasks running" footerKeyFmt = "[::b][↓,j/↑,k][::-] Down/Up [::b][Enter/Esc][::-] Enter/Back [::b][ctrl-c[][::-] Quit" - initErrorFmt = "There is no valid clusters in [red::b]%s[-:-:-]. Please check your aws config. Press any key to exit." colorJSONFmt = `%s"[steelblue::b]%s[-:-:-]": %s` describe = "Describe" @@ -31,6 +30,7 @@ const ( editService = "Edit Service" editTaskDefinition = "Edit Task Definition" + reloadResource = "Reload Resources" openInBrowser = "Open in browser" sshContainer = "SSH container" toggleFullScreen = "JSON Toggle full screen" @@ -46,6 +46,7 @@ const ( lKey = 'l' mKey = 'm' rKey = 'r' + vKey = 'v' tKey = 't' wKey = 'w' @@ -53,6 +54,7 @@ const ( ) var basicKeyInputs = []KeyInput{ + {key: string(rKey), description: reloadResource}, {key: string(bKey), description: openInBrowser}, {key: string(fKey), description: toggleFullScreen}, {key: string(dKey), description: describe}, @@ -169,10 +171,21 @@ func (v *View) handleAppPageSwitch(resourceName string, isJson bool) error { if isJson { kind = v.kind } + v.showKindPage(kind) + return nil +} + +// Reload current resource +func (v *View) reloadResource() error { + v.successModal("Reloaded ✅", 1, 20, 5) + go v.showKindPage(v.kind) + return nil +} - switch kind { +func (v *View) showKindPage(k Kind) error { + switch k { case ClusterPage: - v.app.showClustersPage() + return v.app.showClustersPage() case ServicePage: return v.app.showServicesPage() case TaskPage: