From 80ed68e2d6856a243c6e3b2ae79591a9f9cb9a2a Mon Sep 17 00:00:00 2001 From: Ramana Reddy Date: Tue, 7 Feb 2023 15:38:51 +0530 Subject: [PATCH] fix merge conflicts --- internal/aws/types.go | 4 + internal/config/alias.go | 154 ++++++++++++++++++++++++++++++++++ internal/config/config.go | 4 +- internal/dao/alias.go | 72 ++++++++++++++++ internal/dao/buck_obj.go | 14 ++-- internal/keys.go | 1 + internal/render/alias.go | 49 +++++++++++ internal/render/buck_obj.go | 4 +- internal/ui/dialog/confirm.go | 3 + internal/ui/dialog/error.go | 60 +++++++++++++ internal/ui/prompt.go | 6 +- internal/view/app.go | 38 +++++++-- internal/view/command.go | 36 +++++--- 13 files changed, 412 insertions(+), 33 deletions(-) create mode 100644 internal/config/alias.go create mode 100644 internal/dao/alias.go create mode 100644 internal/render/alias.go create mode 100644 internal/ui/dialog/confirm.go create mode 100644 internal/ui/dialog/error.go diff --git a/internal/aws/types.go b/internal/aws/types.go index bd704da..11c5d32 100644 --- a/internal/aws/types.go +++ b/internal/aws/types.go @@ -15,3 +15,7 @@ type EC2Resp struct { MonitoringState string LaunchTime string } + +type S3Object struct { + Name, ObjectType, LastModified, Size, StorageClass string +} \ No newline at end of file diff --git a/internal/config/alias.go b/internal/config/alias.go new file mode 100644 index 0000000..6c5125a --- /dev/null +++ b/internal/config/alias.go @@ -0,0 +1,154 @@ +package config + +import ( + "os" + "path/filepath" + "sync" + + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" +) + +// CloudlensAlias manages Cloudlens aliases. +var CloudlensAlias = filepath.Join(CloudlensHome(), "alias.yml") + +// Alias tracks shortname to GVR mappings. +type Alias map[string]string + +// ShortNames represents a collection of shortnames for aliases. +type ShortNames map[string][]string + +// Aliases represents a collection of aliases. +type Aliases struct { + Alias Alias `yaml:"alias"` + mx sync.RWMutex +} + +// NewAliases return a new alias. +func NewAliases() *Aliases { + return &Aliases{ + Alias: make(Alias, 50), + } +} + +// Keys returns all aliases keys. +func (a *Aliases) Keys() []string { + a.mx.RLock() + defer a.mx.RUnlock() + + ss := make([]string, 0, len(a.Alias)) + for k := range a.Alias { + ss = append(ss, k) + } + return ss +} + +// ShortNames return all shortnames. +func (a *Aliases) ShortNames() ShortNames { + a.mx.RLock() + defer a.mx.RUnlock() + + m := make(ShortNames, len(a.Alias)) + for alias, res := range a.Alias { + if v, ok := m[res]; ok { + m[res] = append(v, alias) + } else { + m[res] = []string{alias} + } + } + + return m +} + +// Clear remove all aliases. +func (a *Aliases) Clear() { + a.mx.Lock() + defer a.mx.Unlock() + + for k := range a.Alias { + delete(a.Alias, k) + } +} + +// Get retrieves an alias. +func (a *Aliases) Get(k string) (string, bool) { + a.mx.RLock() + defer a.mx.RUnlock() + + v, ok := a.Alias[k] + return v, ok +} + +// Define declares a new alias. +func (a *Aliases) Define(resource string, aliases ...string) { + a.mx.Lock() + defer a.mx.Unlock() + + for _, alias := range aliases { + if _, ok := a.Alias[alias]; ok { + continue + } + a.Alias[alias] = resource + } +} + +// Load Cloudlens aliases. +func (a *Aliases) Load() error { + a.loadDefaultAliases() + return a.LoadFileAliases(CloudlensAlias) +} + +// LoadFileAliases loads alias from a given file. +func (a *Aliases) LoadFileAliases(path string) error { + f, err := os.ReadFile(path) + if err == nil { + var aa Aliases + if err := yaml.Unmarshal(f, &aa); err != nil { + return err + } + + a.mx.Lock() + defer a.mx.Unlock() + for k, v := range aa.Alias { + a.Alias[k] = v + } + } + + return nil +} + +func (a *Aliases) declare(key string, aliases ...string) { + a.Alias[key] = key + for _, alias := range aliases { + a.Alias[alias] = key + } +} + +func (a *Aliases) loadDefaultAliases() { + a.mx.Lock() + defer a.mx.Unlock() + + a.declare("ec2", "Ec2", "EC2") + a.declare("s3", "S3") + a.declare("sg", "SG") + + a.declare("help", "h", "?") + a.declare("quit", "q", "q!", "Q") + a.declare("aliases", "alias", "a") +} + +// Save alias to disk. +func (a *Aliases) Save() error { + log.Debug().Msg("[Config] Saving Aliases...") + return a.SaveAliases(CloudlensAlias) +} + +// SaveAliases saves aliases to a given file. +func (a *Aliases) SaveAliases(path string) error { + EnsurePath(path, DefaultDirMod) + cfg, err := yaml.Marshal(a) + if err != nil { + return err + } + return os.WriteFile(path, cfg, 0644) +} diff --git a/internal/config/config.go b/internal/config/config.go index ba9a0ba..0082cbd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,12 +36,12 @@ type Config struct { // CloudlensHome returns Cloudlens configs home directory. func CloudlensHome() string { if env := os.Getenv(CloudlensConfig); env != "" { - log.Debug().Msg("env CL: " + env) + //log.Debug().Msg("env CL: " + env) return env } xdgCLHome, err := xdg.ConfigFile("cloudlens") - log.Debug().Msg("xdgsclhome: " + xdgCLHome) + //log.Debug().Msg("xdgsclhome: " + xdgCLHome) if err != nil { log.Fatal().Err(err).Msg("Unable to create configuration directory for cloudlens") diff --git a/internal/dao/alias.go b/internal/dao/alias.go new file mode 100644 index 0000000..f7ce500 --- /dev/null +++ b/internal/dao/alias.go @@ -0,0 +1,72 @@ +package dao + +import ( + "context" + "errors" + "fmt" + "sort" + + "github.com/one2nc/cloud-lens/internal" + "github.com/one2nc/cloud-lens/internal/config" + "github.com/one2nc/cloud-lens/internal/render" +) + +var _ Accessor = (*Alias)(nil) + +// Alias tracks standard and custom command aliases. +type Alias struct { + *config.Aliases +} + +// NewAlias returns a new set of aliases. +func NewAlias() *Alias { + a := Alias{Aliases: config.NewAliases()} + + return &a +} + +// Check verifies an alias is defined for this command. +func (a *Alias) Check(cmd string) bool { + _, ok := a.Aliases.Get(cmd) + return ok +} + +// List returns a collection of aliases. +func (a *Alias) List(ctx context.Context) ([]Object, error) { + aa, ok := ctx.Value(internal.KeyAliases).(*Alias) + if !ok { + return nil, fmt.Errorf("expecting *Alias but got %T", ctx.Value(internal.KeyAliases)) + } + m := aa.ShortNames() + oo := make([]Object, 0, len(m)) + for res, aliases := range m { + sort.StringSlice(aliases).Sort() + oo = append(oo, render.AliasRes{Resource: res, Aliases: aliases}) + } + + return oo, nil +} + +// AsResource returns a matching resource if it exists. +func (a *Alias) AsResource(cmd string) (string, bool) { + res, ok := a.Aliases.Get(cmd) + if ok { + return res, true + } + return "", false +} + +// Get fetch a resource. +func (a *Alias) Get(_ context.Context, _ string) (Object, error) { + return nil, errors.New("NYI!!") +} + +// Ensure makes sure alias are loaded. +func (a *Alias) Ensure() (config.Alias, error) { + + return a.Alias, a.load() +} + +func (a *Alias) load() error { + return a.Load() +} diff --git a/internal/dao/buck_obj.go b/internal/dao/buck_obj.go index 125aad7..746449f 100644 --- a/internal/dao/buck_obj.go +++ b/internal/dao/buck_obj.go @@ -29,7 +29,7 @@ func (bo *BObj) List(ctx context.Context) ([]Object, error) { log.Info().Msg(fmt.Sprintf("In Dao Folder Name: %v", fn)) bucketInfo := aws.GetInfoAboutBucket(*sess, bucketName, "/", fn) folderArrayInfo, fileArrayInfo := getBucLevelInfo(bucketInfo) - var s3Objects []S3Object + var s3Objects []aws.S3Object if len(folderArrayInfo) != 0 || len(fileArrayInfo) != 0 { s3Objects = setFoldersAndFIles(bucketInfo.CommonPrefixes, bucketInfo.Contents) } @@ -59,16 +59,14 @@ func getBucLevelInfo(bucketInfo *s3.ListObjectsV2Output) ([]string, []string) { return folderArrayInfo, fileArrayInfo } -type S3Object struct { - Name, ObjectType, LastModified, Size, StorageClass string -} -func setFoldersAndFIles(Folder []*s3.CommonPrefix, File []*s3.Object) []S3Object { - var s3Objects []S3Object + +func setFoldersAndFIles(Folder []*s3.CommonPrefix, File []*s3.Object) []aws.S3Object { + var s3Objects []aws.S3Object indx := 0 for _, bi := range Folder { keyA := strings.Split(*bi.Prefix, "/") - o := S3Object{ + o := aws.S3Object{ Name: keyA[len(keyA)-2], ObjectType: "Folder", LastModified: "-", @@ -81,7 +79,7 @@ func setFoldersAndFIles(Folder []*s3.CommonPrefix, File []*s3.Object) []S3Object for _, fi := range File { keyA := strings.Split(*fi.Key, "/") - o := S3Object{ + o := aws.S3Object{ Name: keyA[len(keyA)-1], ObjectType: "File", LastModified: fi.LastModified.String(), diff --git a/internal/keys.go b/internal/keys.go index f752f6c..50727a2 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -11,4 +11,5 @@ const ( BucketName ContextKey = "bucket_name" ObjectName ContextKey = "object_name" FolderName ContextKey = "folder_name" + KeyAliases ContextKey = "aliases" ) diff --git a/internal/render/alias.go b/internal/render/alias.go new file mode 100644 index 0000000..19e6aed --- /dev/null +++ b/internal/render/alias.go @@ -0,0 +1,49 @@ +package render + +import ( + "fmt" + "strings" +) + +// Alias renders a aliases to screen. +type Alias struct { +} + +// Header returns a header row. +func (Alias) Header(ns string) Header { + return Header{ + HeaderColumn{Name: "RESOURCE"}, + HeaderColumn{Name: "COMMAND"}, + } +} + +// Render renders a K8s resource to screen. +// BOZO!! Pass in a row with pre-alloc fields?? +func (Alias) Render(o interface{}, ns string, r *Row) error { + a, ok := o.(AliasRes) + if !ok { + return fmt.Errorf("expected AliasRes, but got %T", o) + } + + r.ID = a.Resource + r.Fields = append(r.Fields, + a.Resource, + strings.Join(a.Aliases, ","), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// AliasRes represents an alias resource. +type AliasRes struct { + Resource string + Aliases []string +} + +// DeepCopyObject returns a container copy. +func (a AliasRes) DeepCopyObject() interface{} { + return a +} diff --git a/internal/render/buck_obj.go b/internal/render/buck_obj.go index acfab91..88f9dba 100644 --- a/internal/render/buck_obj.go +++ b/internal/render/buck_obj.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/derailed/tview" - "github.com/one2nc/cloud-lens/internal/dao" + "github.com/one2nc/cloud-lens/internal/aws" ) type BObj struct { @@ -21,7 +21,7 @@ func (obj BObj) Header() Header { } func (obj BObj) Render(o interface{}, ns string, row *Row) error { - s3Resp, ok := o.(dao.S3Object) + s3Resp, ok := o.(aws.S3Object) if !ok { return fmt.Errorf("expected S3Resp, but got %T", o) } diff --git a/internal/ui/dialog/confirm.go b/internal/ui/dialog/confirm.go new file mode 100644 index 0000000..b3b3ed4 --- /dev/null +++ b/internal/ui/dialog/confirm.go @@ -0,0 +1,3 @@ +package dialog + +const confirmKey = "confirm" diff --git a/internal/ui/dialog/error.go b/internal/ui/dialog/error.go new file mode 100644 index 0000000..f04771a --- /dev/null +++ b/internal/ui/dialog/error.go @@ -0,0 +1,60 @@ +package dialog + +import ( + "fmt" + "strings" + + "github.com/derailed/tview" + "github.com/gdamore/tcell/v2" + "github.com/one2nc/cloud-lens/internal/ui" +) + +// ShowConfirm pops a confirmation dialog. +func ShowError(pages *ui.Pages, msg string) { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(tcell.ColorDarkSlateBlue). + SetButtonTextColor(tcell.ColorBlack.TrueColor()). + SetLabelColor(tcell.ColorWhite.TrueColor()). + SetFieldTextColor(tcell.ColorIndianRed) + f.AddButton("Dismiss", func() { + dismissError(pages) + }) + if b := f.GetButton(0); b != nil { + b.SetBackgroundColorActivated(tcell.ColorDodgerBlue) + b.SetLabelColorActivated(tcell.ColorBlack.TrueColor()) + } + f.SetFocus(0) + modal := tview.NewModalForm("", f) + modal.SetText(cowTalk(msg)) + modal.SetTextColor(tcell.ColorOrangeRed) + modal.SetBackgroundColor(tcell.ColorBlack.TrueColor()) + modal.SetBorderColor(tcell.ColorBlue) + modal.SetDoneFunc(func(int, string) { + dismissError(pages) + }) + pages.AddPage(confirmKey, modal, false, false) + pages.ShowPage(confirmKey) +} + +func dismissError(pages *ui.Pages) { + pages.RemovePage(confirmKey) +} + +func cowTalk(says string) string { + msg := fmt.Sprintf("< Ruroh? %s >", says) + buff := make([]string, 0, len(cow)+3) + buff = append(buff, msg) + buff = append(buff, cow...) + + return strings.Join(buff, "\n") +} + +var cow = []string{ + `\ ^__^ `, + ` \ (oo)\_______ `, + ` (__)\ )\/\`, + ` ||----w | `, + ` || || `, +} \ No newline at end of file diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index b449951..19e5923 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -98,7 +98,7 @@ func NewPrompt(app *App, noIcons bool) *Prompt { //p.SetBorderAttributes(tcell.AttrUnderline) p.SetBorderPadding(0, 0, 1, 1) p.SetBackgroundColor(tcell.ColorBlack.TrueColor()) - p.SetTextColor(tcell.ColorOrange) + p.SetTextColor(tcell.ColorAquaMarine) p.SetInputCapture(p.keyboard) return &p @@ -191,7 +191,7 @@ func (p *Prompt) write(text, suggest string) { p.SetCursorIndex(p.spacer + len(text)) txt := text if suggest != "" { - txt += fmt.Sprintf("[%s::-]%s", "red", suggest) + txt += fmt.Sprintf("[%s::]%s", "#1E90FF", suggest) } fmt.Fprintf(p, defaultPrompt, p.icon, txt) } @@ -253,7 +253,7 @@ func colorFor(k model.BufferKind) tcell.Color { // nolint:exhaustive switch k { case model.CommandBuffer: - return tcell.ColorAqua + return tcell.ColorMediumAquamarine default: return tcell.ColorSeaGreen } diff --git a/internal/view/app.go b/internal/view/app.go index 8db2e5f..d6b0bf1 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -19,6 +19,7 @@ import ( "github.com/one2nc/cloud-lens/internal/aws" "github.com/one2nc/cloud-lens/internal/config" "github.com/one2nc/cloud-lens/internal/model" + "github.com/one2nc/cloud-lens/internal/ui/dialog" "github.com/one2nc/cloud-lens/internal/ui" "github.com/rs/zerolog/log" @@ -76,6 +77,7 @@ func (a *App) Init(profile, region string, ctx context.Context) error { if err := a.command.Init(); err != nil { return err } + a.CmdBuff().SetSuggestionFn(a.suggestCommand()) // a.layout(ctx) a.tempLayout(ctx) return nil @@ -882,12 +884,6 @@ func (a *App) tempLayout(ctx context.Context) { a.Main.AddPage("main", main, true, false) a.Main.AddPage("splash", ui.NewSplash("0.0.1"), true, true) a.toggleHeader(true) - - //Testing only - //a.inject(NewHelp(a)) - // a.inject(NewEC2("ec2")) - // a.inject(NewSG("sg")) - // a.inject(NewS3("s3")) } // QueueUpdateDraw queues up a ui action and redraw the ui. @@ -958,6 +954,32 @@ func (a *App) buildHeader() tview.Primitive { return header } +func (a *App) suggestCommand() model.SuggestionFunc { + return func(s string) (entries sort.StringSlice) { + // if s == "" { + // if a.cmdHistory.Empty() { + // return + // } + // return a.cmdHistory.List() + // } + + s = strings.ToLower(s) + for _, k := range a.command.alias.Keys() { + if k == s { + continue + } + if strings.HasPrefix(k, s) { + entries = append(entries, strings.Replace(k, s, "", 1)) + } + } + if len(entries) == 0 { + return nil + } + entries.Sort() + return + } +} + func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { if k, ok := a.HasAction(ui.AsKey(evt)); ok && !a.Content.IsTopDialog() { return k.Action(evt) @@ -1011,14 +1033,14 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { func (a *App) gotoResource(cmd, path string, clearStack bool) { err := a.command.run(cmd, path, clearStack) if err != nil { - //dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error()) + dialog.ShowError(a.Content.Pages, err.Error()) } } func (a *App) inject(c model.Component) error { if err := c.Init(a.context); err != nil { log.Error().Err(err).Msgf("component init failed for %q", c.Name()) - //dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error()) + dialog.ShowError(a.Content.Pages, err.Error()) } a.Content.Push(c) diff --git a/internal/view/command.go b/internal/view/command.go index cc81136..161eee3 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -6,6 +6,7 @@ import ( "strings" "sync" + "github.com/one2nc/cloud-lens/internal/dao" "github.com/one2nc/cloud-lens/internal/model" "github.com/rs/zerolog/log" ) @@ -18,9 +19,9 @@ var ( // Command represents a user command. type Command struct { - app *App - - mx sync.Mutex + app *App + alias *dao.Alias + mx sync.Mutex } // NewCommand returns a new command. @@ -32,13 +33,26 @@ func NewCommand(app *App) *Command { // Init initializes the command. func (c *Command) Init() error { + c.alias = dao.NewAlias() + if _, err := c.alias.Ensure(); err != nil { + log.Error().Err(err).Msgf("command init failed!") + return err + } customViewers = loadCustomViewers() return nil } // Reset resets Command and reload aliases. func (c *Command) Reset(clear bool) error { + c.mx.Lock() + defer c.mx.Unlock() + if clear { + c.alias.Clear() + } + if _, err := c.alias.Ensure(); err != nil { + return err + } return nil } @@ -55,6 +69,9 @@ func (c *Command) run(cmd, path string, clearStack bool) error { switch cmds[0] { default: + if !c.alias.Check(cmds[0]) { + return fmt.Errorf("`%s` Command not found", cmd) + } return c.exec(cmd, res, c.componentFor(res, path, v), clearStack) } } @@ -87,18 +104,17 @@ func (c *Command) specialCmd(cmd, path string) bool { } func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { - // gvr, ok := c.alias.AsGVR(cmd) - // if !ok { - // return "", nil, fmt.Errorf("`%s` command not found", cmd) - // } + res, ok := c.alias.AsResource(cmd) + if !ok { + return "", nil, fmt.Errorf("`%s` command not found", cmd) + } - v, ok := customViewers[cmd] - log.Info().Msg(fmt.Sprintf("Is ok: %v", ok)) + v, ok := customViewers[res] if !ok { return cmd, &MetaViewer{viewerFn: NewBrowser}, nil } - return cmd, &v, nil + return res, &v, nil } func (c *Command) componentFor(res, path string, v *MetaViewer) ResourceViewer {