diff --git a/modules/digitalocean/display.go b/modules/digitalocean/display.go index e3cd0d20..ea85e96b 100644 --- a/modules/digitalocean/display.go +++ b/modules/digitalocean/display.go @@ -2,33 +2,60 @@ package digitalocean import ( "fmt" - "strings" "github.com/wtfutil/wtf/utils" ) +const maxColWidth = 12 + func (widget *Widget) content() (string, string, bool) { + columnSet := widget.settings.columns + title := widget.CommonSettings().Title if widget.err != nil { return title, widget.err.Error(), true } - str := fmt.Sprintf( - " [%s]Droplets\n\n", - widget.settings.common.Colors.Subheading, - ) + if len(columnSet) < 1 { + return title, " no columns defined", false + } + + str := fmt.Sprintf(" [::b][%s]", widget.settings.common.Colors.Subheading) + + for _, colName := range columnSet { + truncName := utils.Truncate(colName, maxColWidth, false) + + str += fmt.Sprintf("%-12s", truncName) + } + + str += "\n" for idx, droplet := range widget.droplets { - dropletName := droplet.Name + // This defines the formatting for the row, one tab-seperated string for each defined column + fmtStr := " [%s]" - row := fmt.Sprintf( - "[%s] %-8s %-24s %s", + for range columnSet { + fmtStr += "%-12s" + } + + vals := []interface{}{ widget.RowColor(idx), - droplet.Status, - dropletName, - utils.Truncate(strings.Join(droplet.Tags, ","), 24, true), - ) + } + // Dynamically access the droplet to get the requested columns values + for _, colName := range columnSet { + val, err := droplet.StringValueForProperty(colName) + if err != nil { + val = "???" + } + + truncVal := utils.Truncate(val, maxColWidth, false) + + vals = append(vals, truncVal) + } + + // And format, print, and color the row + row := fmt.Sprintf(fmtStr, vals...) str += utils.HighlightableHelper(widget.View, row, idx, 33) } diff --git a/modules/digitalocean/droplet.go b/modules/digitalocean/droplet.go new file mode 100644 index 00000000..e4e49fc3 --- /dev/null +++ b/modules/digitalocean/droplet.go @@ -0,0 +1,80 @@ +package digitalocean + +import ( + "fmt" + "reflect" + "strings" + + "github.com/digitalocean/godo" + "github.com/wtfutil/wtf/utils" +) + +// Droplet represents WTF's view of a DigitalOcean droplet +type Droplet struct { + godo.Droplet + + Image Image + Region Region +} + +// Image represents WTF's view of a DigitalOcean droplet image +type Image struct { + godo.Image + utils.Reflective +} + +// Region represents WTF's view of a DigitalOcean region +type Region struct { + godo.Region + utils.Reflective +} + +// NewDroplet creates and returns an instance of Droplet +func NewDroplet(doDroplet godo.Droplet) *Droplet { + droplet := &Droplet{ + doDroplet, + + Image{ + *doDroplet.Image, + utils.Reflective{}, + }, + + Region{ + *doDroplet.Region, + utils.Reflective{}, + }, + } + + return droplet +} + +/* -------------------- Exported Functions -------------------- */ + +// StringValueForProperty returns a string value for the given column +func (drop *Droplet) StringValueForProperty(propName string) (string, error) { + var strVal string + var err error + + // Figure out if we should forward this property to a sub-object + // Lets us support "Region.Name" column definitions + split := strings.Split(propName, ".") + + switch split[0] { + case "Image": + strVal, err = drop.Image.StringValueForProperty(split[1]) + case "Region": + strVal, err = drop.Region.StringValueForProperty(split[1]) + default: + v := reflect.ValueOf(drop) + refVal := reflect.Indirect(v).FieldByName(propName) + + if !refVal.IsValid() { + err = fmt.Errorf("invalid property name: %s", propName) + } else { + strVal = fmt.Sprintf("%v", refVal) + } + + } + + return strVal, err +} diff --git a/modules/digitalocean/droplet_properties_table.go b/modules/digitalocean/droplet_properties_table.go index 131c73ba..775b97bd 100644 --- a/modules/digitalocean/droplet_properties_table.go +++ b/modules/digitalocean/droplet_properties_table.go @@ -5,13 +5,12 @@ import ( "strconv" "strings" - "github.com/digitalocean/godo" "github.com/wtfutil/wtf/utils" "github.com/wtfutil/wtf/view" ) type dropletPropertiesTable struct { - droplet *godo.Droplet + droplet *Droplet propertyMap map[string]string colWidth0 int @@ -20,7 +19,7 @@ type dropletPropertiesTable struct { } // newDropletPropertiesTable creates and returns an instance of DropletPropertiesTable -func newDropletPropertiesTable(droplet *godo.Droplet) *dropletPropertiesTable { +func newDropletPropertiesTable(droplet *Droplet) *dropletPropertiesTable { propTable := &dropletPropertiesTable{ droplet: droplet, diff --git a/modules/digitalocean/settings.go b/modules/digitalocean/settings.go index 1d77dcf8..3a46359c 100644 --- a/modules/digitalocean/settings.go +++ b/modules/digitalocean/settings.go @@ -5,6 +5,7 @@ import ( "github.com/olebedev/config" "github.com/wtfutil/wtf/cfg" + "github.com/wtfutil/wtf/utils" "github.com/wtfutil/wtf/wtf" ) @@ -13,12 +14,21 @@ const ( defaultTitle = "DigitalOcean" ) +// defaultColumns defines the default set of columns to display in the widget +// This can be over-ridden in the cofig by explicitly defining a set of columns +var defaultColumns = []interface{}{ + "Name", + "Status", + "Region.Slug", +} + // Settings defines the configuration properties for this module type Settings struct { common *cfg.Common - apiKey string `help:"Your DigitalOcean API key."` - dateFormat string `help:"The format to display dates and times in."` + apiKey string `help:"Your DigitalOcean API key."` + columns []string `help:"A list of the droplet properties to display."` + dateFormat string `help:"The format to display dates and times in."` } // NewSettingsFromYAML creates a new settings instance from a YAML config block @@ -28,6 +38,7 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), apiKey: ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_DIGITALOCEAN_API_KEY"))), + columns: utils.ToStrs(ymlConfig.UList("columns", defaultColumns)), dateFormat: ymlConfig.UString("dateFormat", wtf.DateFormat), } diff --git a/modules/digitalocean/widget.go b/modules/digitalocean/widget.go index fa7cf703..08fb731e 100644 --- a/modules/digitalocean/widget.go +++ b/modules/digitalocean/widget.go @@ -35,10 +35,11 @@ type Widget struct { app *tview.Application client *godo.Client - droplets []godo.Droplet + droplets []*Droplet pages *tview.Pages settings *Settings - err error + + err error } // NewWidget creates a new instance of a widget @@ -127,7 +128,7 @@ func (widget *Widget) createClient() { // currentDroplet returns the currently-selected droplet, if there is one // Returns nil if no droplet is selected -func (widget *Widget) currentDroplet() *godo.Droplet { +func (widget *Widget) currentDroplet() *Droplet { if len(widget.droplets) == 0 { return nil } @@ -136,21 +137,24 @@ func (widget *Widget) currentDroplet() *godo.Droplet { return nil } - return &widget.droplets[widget.Selected] + return widget.droplets[widget.Selected] } // dropletsFetch uses the DigitalOcean API to fetch information about all the available droplets -func (widget *Widget) dropletsFetch() ([]godo.Droplet, error) { - dropletList := []godo.Droplet{} +func (widget *Widget) dropletsFetch() ([]*Droplet, error) { + dropletList := []*Droplet{} opts := &godo.ListOptions{} for { - droplets, resp, err := widget.client.Droplets.List(context.Background(), opts) + doDroplets, resp, err := widget.client.Droplets.List(context.Background(), opts) if err != nil { return dropletList, err } - dropletList = append(dropletList, droplets...) + for _, doDroplet := range doDroplets { + droplet := NewDroplet(doDroplet) + dropletList = append(dropletList, droplet) + } if resp.Links == nil || resp.Links.IsLastPage() { break diff --git a/utils/reflective.go b/utils/reflective.go new file mode 100644 index 00000000..0e5037a4 --- /dev/null +++ b/utils/reflective.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + "reflect" +) + +// Reflective is a convenience wrapper for objects that makes it possible to +// extract property values from the object by property name +type Reflective struct{} + +// StringValueForProperty returns a string value for the given property +// If the property doesn't exist, it returns an error +func (ref *Reflective) StringValueForProperty(propName string) (string, error) { + v := reflect.ValueOf(ref) + refVal := reflect.Indirect(v).FieldByName(propName) + + if !refVal.IsValid() { + return "", fmt.Errorf("invalid property name: %s", propName) + } + + strVal := fmt.Sprintf("%v", refVal) + + return strVal, nil +} diff --git a/utils/text.go b/utils/text.go index 83d22b05..53d9669e 100644 --- a/utils/text.go +++ b/utils/text.go @@ -77,12 +77,13 @@ func Truncate(src string, maxLen int, withEllipse bool) string { return src } -// Formats number as string with 1000 delimiters and, if necessary, rounds it to 2 decimals +// PrettyNumber formats number as string with 1000 delimiters and, if necessary, rounds it to 2 decimals func PrettyNumber(number float64) string { p := message.NewPrinter(language.English) + if number == math.Trunc(number) { return p.Sprintf("%.0f", number) - } else { - return p.Sprintf("%.2f", number) } + + return p.Sprintf("%.2f", number) }