lots of changes

This commit is contained in:
Jesse Duffield 2019-05-19 12:21:40 +10:00
parent 2f1c246fd7
commit 2e9a80fe6c
9 changed files with 171 additions and 68 deletions

View file

@ -162,8 +162,8 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
fmt.Sprintf( fmt.Sprintf(
"%.2f%% Memory (%s/%s)", "%.2f%% Memory (%s/%s)",
memoryUsageHistory[len(memoryUsageHistory)-1], memoryUsageHistory[len(memoryUsageHistory)-1],
formatBinaryBytes(s.MemoryStats.Usage), utils.FormatBinaryBytes(s.MemoryStats.Usage),
formatBinaryBytes(int(s.MemoryStats.Limit)), utils.FormatBinaryBytes(int(s.MemoryStats.Limit)),
), ),
), ),
) )
@ -178,8 +178,8 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
) )
pidsCount := fmt.Sprintf("PIDs: %d", s.PidsStats.Current) pidsCount := fmt.Sprintf("PIDs: %d", s.PidsStats.Current)
dataReceived := fmt.Sprintf("Traffic received: %s", formatDecimalBytes(s.Networks.Eth0.RxBytes)) dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(s.Networks.Eth0.RxBytes))
dataSent := fmt.Sprintf("Traffic sent: %s", formatDecimalBytes(s.Networks.Eth0.TxBytes)) dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(s.Networks.Eth0.TxBytes))
originalJSON, err := json.MarshalIndent(s, "", " ") originalJSON, err := json.MarshalIndent(s, "", " ")
if err != nil { if err != nil {
@ -197,29 +197,3 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
return contents, nil return contents, nil
} }
func formatBinaryBytes(b int) string {
n := float64(b)
units := []string{"B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
for _, unit := range units {
if n > math.Pow(2, 10) {
n = n / math.Pow(2, 10)
} else {
return fmt.Sprintf("%.2f%s", n, unit)
}
}
return "a lot"
}
func formatDecimalBytes(b int) string {
n := float64(b)
units := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
for _, unit := range units {
if n > math.Pow(10, 3) {
n = n / math.Pow(10, 3)
} else {
return fmt.Sprintf("%.2f%s", n, unit)
}
}
return "a lot"
}

View file

@ -78,11 +78,6 @@ func (c *DockerCommand) GetImages() ([]*Image, error) {
for i, image := range images { for i, image := range images {
// func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) // func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error)
history, err := c.Client.ImageHistory(context.Background(), image.ID)
if err != nil {
return nil, err
}
name := "none" name := "none"
tags := image.RepoTags tags := image.RepoTags
if len(tags) > 0 { if len(tags) > 0 {
@ -99,7 +94,6 @@ func (c *DockerCommand) GetImages() ([]*Image, error) {
Client: c.Client, Client: c.Client,
OSCommand: c.OSCommand, OSCommand: c.OSCommand,
Log: c.Log, Log: c.Log,
History: history,
} }
} }

View file

@ -2,7 +2,9 @@ package commands
import ( import (
"context" "context"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/fatih/color" "github.com/fatih/color"
@ -16,7 +18,6 @@ type Image struct {
Tag string Tag string
ID string ID string
Image types.ImageSummary Image types.ImageSummary
History []types.ImageHistory
Client *client.Client Client *client.Client
OSCommand *OSCommand OSCommand *OSCommand
Log *logrus.Entry Log *logrus.Entry
@ -24,7 +25,8 @@ type Image struct {
// GetDisplayStrings returns the display string of Image // GetDisplayStrings returns the display string of Image
func (i *Image) GetDisplayStrings(isFocused bool) []string { func (i *Image) GetDisplayStrings(isFocused bool) []string {
return []string{utils.ColoredString(i.Name, color.FgWhite), utils.ColoredString(i.Tag, color.FgWhite)}
return []string{utils.ColoredString(i.Name, color.FgWhite), utils.ColoredString(i.Tag, color.FgWhite), utils.FormatDecimalBytes(int(i.Image.Size))}
} }
// Remove removes the image // Remove removes the image
@ -35,3 +37,66 @@ func (i *Image) Remove(options types.ImageRemoveOptions) error {
return nil return nil
} }
// Layer is a layer in an image's history
type Layer struct {
types.ImageHistory
}
// GetDisplayStrings returns the array of strings describing the layer
func (l *Layer) GetDisplayStrings(isFocused bool) []string {
tag := ""
if len(l.Tags) > 0 {
tag = l.Tags[0]
}
id := strings.TrimPrefix(l.ID, "sha256:")
if len(id) > 10 {
id = id[0:10]
}
idColor := color.FgWhite
if id == "<missing>" {
idColor = color.FgBlack
}
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
createdBy := l.CreatedBy
if strings.Contains(l.CreatedBy, dockerFileCommandPrefix) {
createdBy = strings.Trim(strings.TrimPrefix(l.CreatedBy, dockerFileCommandPrefix), " ")
split := strings.Split(createdBy, " ")
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
}
createdBy = strings.ReplaceAll(createdBy, "\t", " ")
size := utils.FormatBinaryBytes(int(l.Size))
sizeColor := color.FgWhite
if size == "0B" {
sizeColor = color.FgBlack
}
return []string{
utils.ColoredString(id, idColor),
utils.ColoredString(tag, color.FgGreen),
utils.ColoredString(size, sizeColor),
createdBy,
}
}
// RenderHistory renders the history of the image
func (i *Image) RenderHistory() (string, error) {
history, err := i.Client.ImageHistory(context.Background(), i.ID)
if err != nil {
return "", err
}
i.Log.Warn(spew.Sdump(history))
layers := make([]*Layer, len(history))
for i, layer := range history {
layers[i] = &Layer{layer}
}
return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"}))
}

View file

@ -215,7 +215,7 @@ func (gui *Gui) refreshContainers() error {
containersView.Clear() containersView.Clear()
isFocused := gui.g.CurrentView().Name() == "containers" isFocused := gui.g.CurrentView().Name() == "containers"
list, err := utils.RenderList(gui.State.Containers, isFocused) list, err := utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,8 +1,9 @@
package gui package gui
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"time"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/fatih/color" "github.com/fatih/color"
@ -96,21 +97,25 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
func (gui *Gui) renderImageConfig(mainView *gocui.View, Image *commands.Image, writerID int) error { func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image, writerID int) error {
mainView.Autoscroll = false mainView.Autoscroll = false
mainView.Wrap = false
mainView.Title = "Config" mainView.Title = "Config"
data, err := json.MarshalIndent(&Image.Image, "", " ") output := ""
output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n"
output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
output += utils.WithPadding("Size: ", 10) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
output += utils.WithPadding("Created: ", 10) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
history, err := image.RenderHistory()
if err != nil { if err != nil {
return err return err
} }
historyData, err := json.MarshalIndent(&Image.History, "", " ") output += "\n\n" + history
if err != nil {
return err
}
gui.renderString(gui.g, "main", string(data)+"\n"+string(historyData)) gui.renderString(gui.g, "main", output)
return nil return nil
} }
@ -136,7 +141,7 @@ func (gui *Gui) refreshImages() error {
ImagesView.Clear() ImagesView.Clear()
isFocused := gui.g.CurrentView().Name() == "Images" isFocused := gui.g.CurrentView().Name() == "Images"
list, err := utils.RenderList(gui.State.Images, isFocused) list, err := utils.RenderList(gui.State.Images, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return err return err
} }

View file

@ -54,7 +54,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error { func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu" isFocused := gui.g.CurrentView().Name() == "menu"
gui.State.MenuItemCount = itemCount gui.State.MenuItemCount = itemCount
list, err := utils.RenderList(items, isFocused) list, err := utils.RenderList(items, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return err return err
} }

View file

@ -43,7 +43,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
if err != nil { if err != nil {
panic(err) panic(err)
} }
gui.State.Panels.Main.ObjectKey = "" gui.resetMainView()
return gui.switchFocus(g, v, focusedView) return gui.switchFocus(g, v, focusedView)
} }
@ -68,10 +68,15 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
if err != nil { if err != nil {
panic(err) panic(err)
} }
gui.State.Panels.Main.ObjectKey = "" gui.resetMainView()
return gui.switchFocus(g, v, focusedView) return gui.switchFocus(g, v, focusedView)
} }
func (gui *Gui) resetMainView() {
gui.State.Panels.Main.ObjectKey = ""
gui.getMainView().Wrap = true
}
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
switch v.Name() { switch v.Name() {
case "menu": case "menu":
@ -307,7 +312,7 @@ func (gui *Gui) refreshSelectedLine(line *int, total int) {
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error { func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
isFocused := gui.g.CurrentView().Name() == v.Name() isFocused := gui.g.CurrentView().Name() == v.Name()
list, err := utils.RenderList(items, isFocused) list, err := utils.RenderList(items, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(gui.g, err.Error())
} }

View file

@ -3,6 +3,7 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"reflect" "reflect"
"regexp" "regexp"
"strings" "strings"
@ -95,10 +96,32 @@ type Displayable interface {
GetDisplayStrings(bool) []string GetDisplayStrings(bool) []string
} }
type RenderListConfig struct {
IsFocused bool
Header []string
}
func IsFocused(isFocused bool) func(c *RenderListConfig) {
return func(c *RenderListConfig) {
c.IsFocused = isFocused
}
}
func WithHeader(header []string) func(c *RenderListConfig) {
return func(c *RenderListConfig) {
c.Header = header
}
}
// RenderList takes a slice of items, confirms they implement the Displayable // RenderList takes a slice of items, confirms they implement the Displayable
// interface, then generates a list of their displaystrings to write to a panel's // interface, then generates a list of their displaystrings to write to a panel's
// buffer // buffer
func RenderList(slice interface{}, isFocused bool) (string, error) { func RenderList(slice interface{}, options ...func(*RenderListConfig)) (string, error) {
config := &RenderListConfig{}
for _, option := range options {
option(config)
}
s := reflect.ValueOf(slice) s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice { if s.Kind() != reflect.Slice {
return "", errors.New("RenderList given a non-slice type") return "", errors.New("RenderList given a non-slice type")
@ -114,19 +137,22 @@ func RenderList(slice interface{}, isFocused bool) (string, error) {
displayables[i] = value displayables[i] = value
} }
return renderDisplayableList(displayables, isFocused) return renderDisplayableList(displayables, *config)
} }
// renderDisplayableList takes a list of displayable items, obtains their display // renderDisplayableList takes a list of displayable items, obtains their display
// strings via GetDisplayStrings() and then returns a single string containing // strings via GetDisplayStrings() and then returns a single string containing
// each item's string representation on its own line, with appropriate horizontal // each item's string representation on its own line, with appropriate horizontal
// padding between the item's own strings // padding between the item's own strings
func renderDisplayableList(items []Displayable, isFocused bool) (string, error) { func renderDisplayableList(items []Displayable, config RenderListConfig) (string, error) {
if len(items) == 0 { if len(items) == 0 {
return "", nil return "", nil
} }
stringArrays := getDisplayStringArrays(items, isFocused) stringArrays := getDisplayStringArrays(items, config.IsFocused)
if len(config.Header) > 0 {
stringArrays = append([][]string{config.Header}, stringArrays...)
}
if !displayArraysAligned(stringArrays) { if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display") return "", errors.New("Each item must return the same number of strings to display")
@ -228,3 +254,37 @@ func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ") bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes) return string(bytes)
} }
func FormatBinaryBytes(b int) string {
n := float64(b)
units := []string{"B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
for _, unit := range units {
if n > math.Pow(2, 10) {
n = n / math.Pow(2, 10)
} else {
val := fmt.Sprintf("%.2f%s", n, unit)
if val == "0.00B" {
return "0B"
}
return val
}
}
return "a lot"
}
func FormatDecimalBytes(b int) string {
n := float64(b)
units := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
for _, unit := range units {
if n > math.Pow(10, 3) {
n = n / math.Pow(10, 3)
} else {
val := fmt.Sprintf("%.2f%s", n, unit)
if val == "0.00B" {
return "0B"
}
return val
}
}
return "a lot"
}

View file

@ -246,7 +246,7 @@ func TestGetDisplayStringArrays(t *testing.T) {
func TestRenderDisplayableList(t *testing.T) { func TestRenderDisplayableList(t *testing.T) {
type scenario struct { type scenario struct {
input []Displayable input []Displayable
isFocused bool config RenderListConfig
expectedString string expectedString string
expectedErrorMessage string expectedErrorMessage string
} }
@ -257,7 +257,7 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{}}), Displayable(&myDisplayable{[]string{}}),
Displayable(&myDisplayable{[]string{}}), Displayable(&myDisplayable{[]string{}}),
}, },
false, RenderListConfig{},
"\n", "\n",
"", "",
}, },
@ -266,7 +266,7 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{"aa", "b"}}), Displayable(&myDisplayable{[]string{"aa", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}), Displayable(&myDisplayable{[]string{"c", "d"}}),
}, },
false, RenderListConfig{},
"aa b\nc d", "aa b\nc d",
"", "",
}, },
@ -275,7 +275,7 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{"a"}}), Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b", "c"}}), Displayable(&myDisplayable{[]string{"b", "c"}}),
}, },
false, RenderListConfig{},
"", "",
"Each item must return the same number of strings to display", "Each item must return the same number of strings to display",
}, },
@ -284,14 +284,14 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{"a"}}), Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b"}}), Displayable(&myDisplayable{[]string{"b"}}),
}, },
true, RenderListConfig{IsFocused: true},
"a blah\nb blah", "a blah\nb blah",
"", "",
}, },
} }
for _, s := range scenarios { for _, s := range scenarios {
str, err := renderDisplayableList(s.input, s.isFocused) str, err := renderDisplayableList(s.input, s.config)
assert.EqualValues(t, s.expectedString, str) assert.EqualValues(t, s.expectedString, str)
if s.expectedErrorMessage != "" { if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage) assert.EqualError(t, err, s.expectedErrorMessage)
@ -305,7 +305,7 @@ func TestRenderDisplayableList(t *testing.T) {
func TestRenderList(t *testing.T) { func TestRenderList(t *testing.T) {
type scenario struct { type scenario struct {
input interface{} input interface{}
isFocused bool options []func(*RenderListConfig)
expectedString string expectedString string
expectedErrorMessage string expectedErrorMessage string
} }
@ -316,7 +316,7 @@ func TestRenderList(t *testing.T) {
{[]string{"aa", "b"}}, {[]string{"aa", "b"}},
{[]string{"c", "d"}}, {[]string{"c", "d"}},
}, },
false, nil,
"aa b\nc d", "aa b\nc d",
"", "",
}, },
@ -325,13 +325,13 @@ func TestRenderList(t *testing.T) {
{}, {},
{}, {},
}, },
false, nil,
"", "",
"item does not implement the Displayable interface", "item does not implement the Displayable interface",
}, },
{ {
&myStruct{}, &myStruct{},
false, nil,
"", "",
"RenderList given a non-slice type", "RenderList given a non-slice type",
}, },
@ -339,14 +339,14 @@ func TestRenderList(t *testing.T) {
[]*myDisplayable{ []*myDisplayable{
{[]string{"a"}}, {[]string{"a"}},
}, },
true, []func(*RenderListConfig){IsFocused(true)},
"a blah", "a blah",
"", "",
}, },
} }
for _, s := range scenarios { for _, s := range scenarios {
str, err := RenderList(s.input, s.isFocused) str, err := RenderList(s.input, s.options...)
assert.EqualValues(t, s.expectedString, str) assert.EqualValues(t, s.expectedString, str)
if s.expectedErrorMessage != "" { if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage) assert.EqualError(t, err, s.expectedErrorMessage)