From 2e9a80fe6ca1bedb8313f3f30ba18a5312e7950f Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 19 May 2019 12:21:40 +1000 Subject: [PATCH] lots of changes --- pkg/commands/container_stats.go | 34 ++-------------- pkg/commands/docker.go | 6 --- pkg/commands/image.go | 69 ++++++++++++++++++++++++++++++++- pkg/gui/containers_panel.go | 2 +- pkg/gui/images_panel.go | 23 ++++++----- pkg/gui/menu_panel.go | 2 +- pkg/gui/view_helpers.go | 11 ++++-- pkg/utils/utils.go | 68 ++++++++++++++++++++++++++++++-- pkg/utils/utils_test.go | 24 ++++++------ 9 files changed, 171 insertions(+), 68 deletions(-) diff --git a/pkg/commands/container_stats.go b/pkg/commands/container_stats.go index 48af245a..cfc47179 100644 --- a/pkg/commands/container_stats.go +++ b/pkg/commands/container_stats.go @@ -162,8 +162,8 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m fmt.Sprintf( "%.2f%% Memory (%s/%s)", memoryUsageHistory[len(memoryUsageHistory)-1], - formatBinaryBytes(s.MemoryStats.Usage), - formatBinaryBytes(int(s.MemoryStats.Limit)), + utils.FormatBinaryBytes(s.MemoryStats.Usage), + 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) - dataReceived := fmt.Sprintf("Traffic received: %s", formatDecimalBytes(s.Networks.Eth0.RxBytes)) - dataSent := fmt.Sprintf("Traffic sent: %s", formatDecimalBytes(s.Networks.Eth0.TxBytes)) + dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(s.Networks.Eth0.RxBytes)) + dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(s.Networks.Eth0.TxBytes)) originalJSON, err := json.MarshalIndent(s, "", " ") if err != nil { @@ -197,29 +197,3 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m 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" -} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 4504ab76..c1219cdd 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -78,11 +78,6 @@ func (c *DockerCommand) GetImages() ([]*Image, error) { for i, image := range images { // 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" tags := image.RepoTags if len(tags) > 0 { @@ -99,7 +94,6 @@ func (c *DockerCommand) GetImages() ([]*Image, error) { Client: c.Client, OSCommand: c.OSCommand, Log: c.Log, - History: history, } } diff --git a/pkg/commands/image.go b/pkg/commands/image.go index 44f87815..37b16c84 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -2,7 +2,9 @@ package commands import ( "context" + "strings" + "github.com/davecgh/go-spew/spew" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/fatih/color" @@ -16,7 +18,6 @@ type Image struct { Tag string ID string Image types.ImageSummary - History []types.ImageHistory Client *client.Client OSCommand *OSCommand Log *logrus.Entry @@ -24,7 +25,8 @@ type Image struct { // GetDisplayStrings returns the display string of Image 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 @@ -35,3 +37,66 @@ func (i *Image) Remove(options types.ImageRemoveOptions) error { 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 == "" { + 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"})) +} diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index db012140..683d090e 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -215,7 +215,7 @@ func (gui *Gui) refreshContainers() error { containersView.Clear() 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 { return err } diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 4ad7074f..8b943dd8 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -1,8 +1,9 @@ package gui import ( - "encoding/json" "fmt" + "strings" + "time" "github.com/docker/docker/api/types" "github.com/fatih/color" @@ -96,21 +97,25 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { 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.Wrap = false 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 { return err } - historyData, err := json.MarshalIndent(&Image.History, "", " ") - if err != nil { - return err - } + output += "\n\n" + history - gui.renderString(gui.g, "main", string(data)+"\n"+string(historyData)) + gui.renderString(gui.g, "main", output) return nil } @@ -136,7 +141,7 @@ func (gui *Gui) refreshImages() error { ImagesView.Clear() 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 { return err } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 3d4d305b..93248727 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -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 { isFocused := gui.g.CurrentView().Name() == "menu" gui.State.MenuItemCount = itemCount - list, err := utils.RenderList(items, isFocused) + list, err := utils.RenderList(items, utils.IsFocused(isFocused)) if err != nil { return err } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index d386e971..14df07a4 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -43,7 +43,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { if err != nil { panic(err) } - gui.State.Panels.Main.ObjectKey = "" + gui.resetMainView() return gui.switchFocus(g, v, focusedView) } @@ -68,10 +68,15 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { if err != nil { panic(err) } - gui.State.Panels.Main.ObjectKey = "" + gui.resetMainView() 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 { switch v.Name() { case "menu": @@ -307,7 +312,7 @@ func (gui *Gui) refreshSelectedLine(line *int, total int) { func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error { gui.g.Update(func(g *gocui.Gui) error { isFocused := gui.g.CurrentView().Name() == v.Name() - list, err := utils.RenderList(items, isFocused) + list, err := utils.RenderList(items, utils.IsFocused(isFocused)) if err != nil { return gui.createErrorPanel(gui.g, err.Error()) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index e5a9e98a..506edae0 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "encoding/json" "fmt" + "math" "reflect" "regexp" "strings" @@ -95,10 +96,32 @@ type Displayable interface { 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 // interface, then generates a list of their displaystrings to write to a panel's // 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) if s.Kind() != reflect.Slice { return "", errors.New("RenderList given a non-slice type") @@ -114,19 +137,22 @@ func RenderList(slice interface{}, isFocused bool) (string, error) { displayables[i] = value } - return renderDisplayableList(displayables, isFocused) + return renderDisplayableList(displayables, *config) } // renderDisplayableList takes a list of displayable items, obtains their display // strings via GetDisplayStrings() and then returns a single string containing // each item's string representation on its own line, with appropriate horizontal // 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 { 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) { 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, "", " ") 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" +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 1e232272..6b76a084 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -246,7 +246,7 @@ func TestGetDisplayStringArrays(t *testing.T) { func TestRenderDisplayableList(t *testing.T) { type scenario struct { input []Displayable - isFocused bool + config RenderListConfig expectedString string expectedErrorMessage string } @@ -257,7 +257,7 @@ func TestRenderDisplayableList(t *testing.T) { Displayable(&myDisplayable{[]string{}}), Displayable(&myDisplayable{[]string{}}), }, - false, + RenderListConfig{}, "\n", "", }, @@ -266,7 +266,7 @@ func TestRenderDisplayableList(t *testing.T) { Displayable(&myDisplayable{[]string{"aa", "b"}}), Displayable(&myDisplayable{[]string{"c", "d"}}), }, - false, + RenderListConfig{}, "aa b\nc d", "", }, @@ -275,7 +275,7 @@ func TestRenderDisplayableList(t *testing.T) { Displayable(&myDisplayable{[]string{"a"}}), Displayable(&myDisplayable{[]string{"b", "c"}}), }, - false, + RenderListConfig{}, "", "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{"b"}}), }, - true, + RenderListConfig{IsFocused: true}, "a blah\nb blah", "", }, } for _, s := range scenarios { - str, err := renderDisplayableList(s.input, s.isFocused) + str, err := renderDisplayableList(s.input, s.config) assert.EqualValues(t, s.expectedString, str) if s.expectedErrorMessage != "" { assert.EqualError(t, err, s.expectedErrorMessage) @@ -305,7 +305,7 @@ func TestRenderDisplayableList(t *testing.T) { func TestRenderList(t *testing.T) { type scenario struct { input interface{} - isFocused bool + options []func(*RenderListConfig) expectedString string expectedErrorMessage string } @@ -316,7 +316,7 @@ func TestRenderList(t *testing.T) { {[]string{"aa", "b"}}, {[]string{"c", "d"}}, }, - false, + nil, "aa b\nc d", "", }, @@ -325,13 +325,13 @@ func TestRenderList(t *testing.T) { {}, {}, }, - false, + nil, "", "item does not implement the Displayable interface", }, { &myStruct{}, - false, + nil, "", "RenderList given a non-slice type", }, @@ -339,14 +339,14 @@ func TestRenderList(t *testing.T) { []*myDisplayable{ {[]string{"a"}}, }, - true, + []func(*RenderListConfig){IsFocused(true)}, "a blah", "", }, } for _, s := range scenarios { - str, err := RenderList(s.input, s.isFocused) + str, err := RenderList(s.input, s.options...) assert.EqualValues(t, s.expectedString, str) if s.expectedErrorMessage != "" { assert.EqualError(t, err, s.expectedErrorMessage)