mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
add volumes panel
This commit is contained in:
parent
84090bcb94
commit
87c6208577
13 changed files with 519 additions and 80 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
|
|
@ -400,3 +401,9 @@ func (c *Container) ViewLogs() (*exec.Cmd, error) {
|
|||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// PruneContainers prunes containers
|
||||
func (c *DockerCommand) PruneContainers() error {
|
||||
_, err := c.Client.ContainersPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"github.com/acarl005/stripansi"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
|
|
@ -37,6 +36,7 @@ type DockerCommand struct {
|
|||
// DisplayContainers is the array of containers we will display in the containers panel. If Gui.ShowAllContainers is false, this will only be those containers which aren't based on a service. This reduces clutter and duplication in the UI
|
||||
DisplayContainers []*Container
|
||||
Images []*Image
|
||||
Volumes []*Volume
|
||||
}
|
||||
|
||||
// NewDockerCommand it runs git commands
|
||||
|
|
@ -148,8 +148,8 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetContainersAndServices returns a slice of docker containers
|
||||
func (c *DockerCommand) GetContainersAndServices() error {
|
||||
// RefreshContainersAndServices returns a slice of docker containers
|
||||
func (c *DockerCommand) RefreshContainersAndServices() error {
|
||||
c.ServiceMutex.Lock()
|
||||
defer c.ServiceMutex.Unlock()
|
||||
|
||||
|
|
@ -344,56 +344,6 @@ func (c *DockerCommand) UpdateContainerDetails() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetImages returns a slice of docker images
|
||||
func (c *DockerCommand) GetImages() ([]*Image, error) {
|
||||
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownImages := make([]*Image, len(images))
|
||||
|
||||
for i, image := range images {
|
||||
// func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error)
|
||||
|
||||
name := "none"
|
||||
tags := image.RepoTags
|
||||
if len(tags) > 0 {
|
||||
name = tags[0]
|
||||
}
|
||||
|
||||
nameParts := strings.Split(name, ":")
|
||||
tag := ""
|
||||
if len(nameParts) > 1 {
|
||||
tag = nameParts[1]
|
||||
}
|
||||
|
||||
ownImages[i] = &Image{
|
||||
ID: image.ID,
|
||||
Name: nameParts[0],
|
||||
Tag: tag,
|
||||
Image: image,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
}
|
||||
}
|
||||
|
||||
return ownImages, nil
|
||||
}
|
||||
|
||||
// PruneImages prunes images
|
||||
func (c *DockerCommand) PruneImages() error {
|
||||
_, err := c.Client.ImagesPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
||||
// PruneContainers prunes containers
|
||||
func (c *DockerCommand) PruneContainers() error {
|
||||
_, err := c.Client.ContainersPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
||||
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
||||
func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
|
||||
cmd := c.OSCommand.ExecutableFromString(c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
|
|
@ -97,3 +98,50 @@ func (i *Image) RenderHistory() (string, error) {
|
|||
|
||||
return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// GetImages returns a slice of docker images
|
||||
func (c *DockerCommand) GetImages() ([]*Image, error) {
|
||||
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownImages := make([]*Image, len(images))
|
||||
|
||||
for i, image := range images {
|
||||
// func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error)
|
||||
|
||||
name := "none"
|
||||
tags := image.RepoTags
|
||||
if len(tags) > 0 {
|
||||
name = tags[0]
|
||||
}
|
||||
|
||||
nameParts := strings.Split(name, ":")
|
||||
tag := ""
|
||||
if len(nameParts) > 1 {
|
||||
tag = nameParts[1]
|
||||
}
|
||||
|
||||
ownImages[i] = &Image{
|
||||
ID: image.ID,
|
||||
Name: nameParts[0],
|
||||
Tag: tag,
|
||||
Image: image,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
}
|
||||
}
|
||||
|
||||
return ownImages, nil
|
||||
}
|
||||
|
||||
// PruneImages prunes images
|
||||
func (c *DockerCommand) PruneImages() error {
|
||||
_, err := c.Client.ImagesPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
66
pkg/commands/volume.go
Normal file
66
pkg/commands/volume.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Volume : A docker Volume
|
||||
type Volume struct {
|
||||
Name string
|
||||
Volume *types.Volume
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of Container
|
||||
func (v *Volume) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{v.Volume.Driver, v.Name}
|
||||
}
|
||||
|
||||
// RefreshVolumes gets the volumes and stores them
|
||||
func (c *DockerCommand) RefreshVolumes() error {
|
||||
result, err := c.Client.VolumeList(context.Background(), filters.Args{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes := result.Volumes
|
||||
|
||||
ownVolumes := make([]*Volume, len(volumes))
|
||||
|
||||
sort.Slice(volumes, func(i, j int) bool {
|
||||
return volumes[i].Name < volumes[j].Name
|
||||
})
|
||||
|
||||
for i, volume := range volumes {
|
||||
ownVolumes[i] = &Volume{
|
||||
Name: volume.Name,
|
||||
Volume: volume,
|
||||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
}
|
||||
}
|
||||
|
||||
c.Volumes = ownVolumes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PruneVolumes prunes volumes
|
||||
func (c *DockerCommand) PruneVolumes() error {
|
||||
_, err := c.Client.VolumesPrune(context.Background(), filters.Args{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove removes the volume
|
||||
func (v *Volume) Remove(force bool) error {
|
||||
return v.Client.VolumeRemove(context.Background(), v.Name, force)
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
key := container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex]
|
||||
key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex]
|
||||
if gui.State.Panels.Main.ObjectKey == key {
|
||||
return nil
|
||||
} else {
|
||||
|
|
@ -243,7 +243,7 @@ func (gui *Gui) refreshContainersAndServices() error {
|
|||
selectedService = gui.DockerCommand.Services[sl]
|
||||
}
|
||||
|
||||
if err := gui.refreshStateContainersAndServices(); err != nil {
|
||||
if err := gui.DockerCommand.RefreshContainersAndServices(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -314,10 +314,6 @@ func (gui *Gui) refreshContainersAndServices() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateContainersAndServices() error {
|
||||
return gui.DockerCommand.GetContainersAndServices()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type SentinelErrors struct {
|
|||
ErrSubProcess error
|
||||
ErrNoContainers error
|
||||
ErrNoImages error
|
||||
ErrNoVolumes error
|
||||
}
|
||||
|
||||
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
|
||||
|
|
@ -47,6 +48,7 @@ func (gui *Gui) GenerateSentinelErrors() {
|
|||
ErrSubProcess: errors.New(gui.Tr.RunningSubprocess),
|
||||
ErrNoContainers: errors.New(gui.Tr.NoContainers),
|
||||
ErrNoImages: errors.New(gui.Tr.NoImages),
|
||||
ErrNoVolumes: errors.New(gui.Tr.NoVolumes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,12 +96,18 @@ type imagePanelState struct {
|
|||
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
||||
}
|
||||
|
||||
type volumePanelState struct {
|
||||
SelectedLine int
|
||||
ContextIndex int
|
||||
}
|
||||
|
||||
type panelStates struct {
|
||||
Services *servicePanelState
|
||||
Containers *containerPanelState
|
||||
Menu *menuPanelState
|
||||
Main *mainPanelState
|
||||
Images *imagePanelState
|
||||
Volumes *volumePanelState
|
||||
Status *statusState
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +130,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
|
|||
Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0},
|
||||
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
|
||||
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
|
||||
Volumes: &volumePanelState{SelectedLine: -1, ContextIndex: 0},
|
||||
Menu: &menuPanelState{SelectedLine: 0},
|
||||
Main: &mainPanelState{
|
||||
ObjectKey: "",
|
||||
|
|
@ -245,6 +254,7 @@ func (gui *Gui) Run() error {
|
|||
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
|
||||
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
|
||||
gui.goEvery(time.Millisecond*100, gui.refreshContainersAndServices)
|
||||
gui.goEvery(time.Millisecond*100, gui.refreshVolumes)
|
||||
gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails)
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ func (gui *Gui) handleImagesFocus(g *gocui.Gui, v *gocui.View) error {
|
|||
|
||||
gui.State.Panels.Images.SelectedLine = newSelectedLine
|
||||
|
||||
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
|
||||
return gui.handleImagePress(gui.g, v)
|
||||
} else {
|
||||
if !(prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name()) {
|
||||
return gui.handleImageSelect(gui.g, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
|
@ -69,7 +69,7 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
|
|||
return gui.renderString(g, "main", gui.Tr.NoImages)
|
||||
}
|
||||
|
||||
key := Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
|
||||
key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
|
||||
if gui.State.Panels.Main.ObjectKey == key {
|
||||
return nil
|
||||
} else {
|
||||
|
|
@ -98,11 +98,13 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
|
|||
|
||||
func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) error {
|
||||
go gui.T.NewTask(func(stop chan struct{}) {
|
||||
padding := 10
|
||||
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"
|
||||
output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
|
||||
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
|
||||
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
|
||||
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
|
||||
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
|
||||
|
||||
history, err := image.RenderHistory()
|
||||
if err != nil {
|
||||
|
|
@ -156,6 +158,7 @@ func (gui *Gui) refreshImages() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: leave this to DockerCommand
|
||||
func (gui *Gui) refreshStateImages() error {
|
||||
Images, err := gui.DockerCommand.GetImages()
|
||||
if err != nil {
|
||||
|
|
@ -189,10 +192,6 @@ func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
|||
return gui.handleImageSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleImagePress(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleImagesNextContext(g *gocui.Gui, v *gocui.View) error {
|
||||
contexts := gui.getImageContexts()
|
||||
if gui.State.Panels.Images.ContextIndex >= len(contexts)-1 {
|
||||
|
|
|
|||
|
|
@ -312,10 +312,38 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Handler: gui.handlePruneImages,
|
||||
Description: gui.Tr.PruneImages,
|
||||
},
|
||||
{
|
||||
ViewName: "volumes",
|
||||
Key: '[',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleVolumesPrevContext,
|
||||
Description: gui.Tr.PreviousContext,
|
||||
},
|
||||
{
|
||||
ViewName: "volumes",
|
||||
Key: ']',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleVolumesNextContext,
|
||||
Description: gui.Tr.NextContext,
|
||||
},
|
||||
{
|
||||
ViewName: "volumes",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleVolumesRemoveMenu,
|
||||
Description: gui.Tr.RemoveVolume,
|
||||
},
|
||||
{
|
||||
ViewName: "volumes",
|
||||
Key: 'D',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePruneVolumes,
|
||||
Description: gui.Tr.PruneVolumes,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: add more views here
|
||||
for _, viewName := range []string{"status", "services", "containers", "images", "menu"} {
|
||||
for _, viewName := range []string{"status", "services", "containers", "images", "volumes", "menu"} {
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||
|
|
@ -334,6 +362,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
"services": {prevLine: gui.handleServicesPrevLine, nextLine: gui.handleServicesNextLine, focus: gui.handleServicesFocus},
|
||||
"containers": {prevLine: gui.handleContainersPrevLine, nextLine: gui.handleContainersNextLine, focus: gui.handleContainersFocus},
|
||||
"images": {prevLine: gui.handleImagesPrevLine, nextLine: gui.handleImagesNextLine, focus: gui.handleImagesFocus},
|
||||
"volumes": {prevLine: gui.handleVolumesPrevLine, nextLine: gui.handleVolumesNextLine, focus: gui.handleVolumesFocus},
|
||||
}
|
||||
|
||||
for viewName, functions := range listPanelMap {
|
||||
|
|
|
|||
|
|
@ -92,13 +92,14 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||
|
||||
usableSpace := height - 4
|
||||
|
||||
tallPanels := 3
|
||||
tallPanels := 4
|
||||
|
||||
vHeights := map[string]int{
|
||||
"status": tallPanels,
|
||||
"status": 3,
|
||||
"services": usableSpace/tallPanels + usableSpace%tallPanels,
|
||||
"containers": usableSpace / tallPanels,
|
||||
"images": usableSpace / tallPanels,
|
||||
"volumes": usableSpace / tallPanels,
|
||||
"options": 1,
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +113,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||
"services": defaultHeight,
|
||||
"containers": defaultHeight,
|
||||
"images": defaultHeight,
|
||||
"volumes": defaultHeight,
|
||||
"options": defaultHeight,
|
||||
}
|
||||
vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1
|
||||
|
|
@ -186,6 +188,18 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||
gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), imagesView)
|
||||
}
|
||||
|
||||
volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"])
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
volumesView.Highlight = true
|
||||
volumesView.Title = gui.Tr.VolumesTitle
|
||||
volumesView.FgColor = gocui.ColorWhite
|
||||
|
||||
gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), volumesView)
|
||||
}
|
||||
|
||||
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
|
|
@ -244,6 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||
listViews := map[*gocui.View]listViewState{
|
||||
containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.Containers)},
|
||||
imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
|
||||
volumesView: {selectedLine: gui.State.Panels.Volumes.SelectedLine, lineCount: len(gui.DockerCommand.Volumes)},
|
||||
}
|
||||
|
||||
// menu view might not exist so we check to be safe
|
||||
|
|
@ -252,8 +267,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||
}
|
||||
for view, state := range listViews {
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
|
||||
return err
|
||||
if view == gui.g.CurrentView() {
|
||||
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
key := service.ID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex]
|
||||
key := "services-" + service.ID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex]
|
||||
if gui.State.Panels.Main.ObjectKey == key {
|
||||
return nil
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/spkg/bom"
|
||||
)
|
||||
|
||||
var cyclableViews = []string{"status", "services", "containers", "images"}
|
||||
var cyclableViews = []string{"status", "services", "containers", "images", "volumes"}
|
||||
|
||||
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
||||
// not refreshing containers and services here given that we do it every few milliseconds anyway
|
||||
|
|
@ -91,6 +91,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
|||
return gui.handleContainerSelect(g, v)
|
||||
case "images":
|
||||
return gui.handleImageSelect(g, v)
|
||||
case "volumes":
|
||||
return gui.handleVolumeSelect(g, v)
|
||||
case "confirmation":
|
||||
return nil
|
||||
case "main":
|
||||
|
|
@ -245,6 +247,11 @@ func (gui *Gui) getImagesView() *gocui.View {
|
|||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getVolumesView() *gocui.View {
|
||||
v, _ := gui.g.View("volumes")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getMainView() *gocui.View {
|
||||
v, _ := gui.g.View("main")
|
||||
return v
|
||||
|
|
|
|||
298
pkg/gui/volumes_panel.go
Normal file
298
pkg/gui/volumes_panel.go
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getVolumeContexts() []string {
|
||||
return []string{"config"}
|
||||
}
|
||||
|
||||
func (gui *Gui) getVolumeContextTitles() []string {
|
||||
return []string{gui.Tr.ConfigTitle}
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedVolume() (*commands.Volume, error) {
|
||||
selectedLine := gui.State.Panels.Volumes.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil, gui.Errors.ErrNoVolumes
|
||||
}
|
||||
|
||||
return gui.DockerCommand.Volumes[selectedLine], nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumesFocus(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cx, cy := v.Cursor()
|
||||
_, oy := v.Origin()
|
||||
|
||||
prevSelectedLine := gui.State.Panels.Volumes.SelectedLine
|
||||
newSelectedLine := cy - oy
|
||||
|
||||
if newSelectedLine > len(gui.DockerCommand.Volumes)-1 || len(utils.Decolorise(gui.DockerCommand.Volumes[newSelectedLine].Name)) < cx {
|
||||
return gui.handleVolumeSelect(gui.g, v)
|
||||
}
|
||||
|
||||
gui.State.Panels.Volumes.SelectedLine = newSelectedLine
|
||||
|
||||
if !(prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name()) {
|
||||
return gui.handleVolumeSelect(gui.g, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumeSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volume, err := gui.getSelectedVolume()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoVolumes {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", gui.Tr.NoVolumes)
|
||||
}
|
||||
|
||||
key := "volumes-" + volume.Name + "-" + gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex]
|
||||
if gui.State.Panels.Main.ObjectKey == key {
|
||||
return nil
|
||||
} else {
|
||||
gui.State.Panels.Main.ObjectKey = key
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mainView := gui.getMainView()
|
||||
mainView.Tabs = gui.getVolumeContextTitles()
|
||||
mainView.TabIndex = gui.State.Panels.Volumes.ContextIndex
|
||||
|
||||
switch gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex] {
|
||||
case "config":
|
||||
if err := gui.renderVolumeConfig(mainView, volume); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("Unknown context for Volumes panel")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderVolumeConfig(mainView *gocui.View, volume *commands.Volume) error {
|
||||
go gui.T.NewTask(func(stop chan struct{}) {
|
||||
mainView.Autoscroll = false
|
||||
mainView.Wrap = true
|
||||
|
||||
padding := 15
|
||||
output := ""
|
||||
output += utils.WithPadding("Name: ", padding) + volume.Name + "\n"
|
||||
output += utils.WithPadding("Driver: ", padding) + volume.Volume.Driver + "\n"
|
||||
output += utils.WithPadding("Scope: ", padding) + volume.Volume.Scope + "\n"
|
||||
output += utils.WithPadding("Mountpoint: ", padding) + volume.Volume.Mountpoint + "\n"
|
||||
output += utils.WithPadding("Labels: ", padding)
|
||||
if len(volume.Volume.Labels) > 0 {
|
||||
output += "\n"
|
||||
for k, v := range volume.Volume.Labels {
|
||||
output += formatMapItem(padding, k, v)
|
||||
}
|
||||
} else {
|
||||
output += "none\n"
|
||||
}
|
||||
|
||||
output += "\n" + utils.WithPadding("Options: ", padding)
|
||||
if len(volume.Volume.Options) > 0 {
|
||||
output += "\n"
|
||||
for k, v := range volume.Volume.Options {
|
||||
output += formatMapItem(padding, k, v)
|
||||
}
|
||||
} else {
|
||||
output += "none\n"
|
||||
}
|
||||
|
||||
output += "\n" + utils.WithPadding("Status: ", padding)
|
||||
if volume.Volume.Status != nil {
|
||||
output += "\n"
|
||||
for k, v := range volume.Volume.Status {
|
||||
output += formatMapItem(padding, k, v)
|
||||
}
|
||||
} else {
|
||||
output += "n/a"
|
||||
}
|
||||
|
||||
if volume.Volume.UsageData != nil {
|
||||
output += utils.WithPadding("RefCount: ", padding) + string(volume.Volume.UsageData.RefCount) + "\n"
|
||||
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n"
|
||||
}
|
||||
|
||||
gui.renderString(gui.g, "main", output)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatMapItem(padding int, k string, v interface{}) string {
|
||||
return fmt.Sprintf("%s%s %v\n", strings.Repeat(" ", padding), utils.ColoredString(k+":", color.FgYellow), utils.ColoredString(fmt.Sprintf("%v", v), color.FgMagenta))
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshVolumes() error {
|
||||
volumesView := gui.getVolumesView()
|
||||
if volumesView == nil {
|
||||
// if the volumesView hasn't been instantiated yet we just return
|
||||
return nil
|
||||
}
|
||||
if err := gui.DockerCommand.RefreshVolumes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gui.DockerCommand.Volumes) > 0 && gui.State.Panels.Volumes.SelectedLine == -1 {
|
||||
gui.State.Panels.Volumes.SelectedLine = 0
|
||||
}
|
||||
if len(gui.DockerCommand.Volumes)-1 < gui.State.Panels.Volumes.SelectedLine {
|
||||
gui.State.Panels.Volumes.SelectedLine = len(gui.DockerCommand.Volumes) - 1
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
volumesView.Clear()
|
||||
isFocused := gui.g.CurrentView().Name() == "volumes"
|
||||
list, err := utils.RenderList(gui.DockerCommand.Volumes, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(volumesView, list)
|
||||
|
||||
if volumesView == g.CurrentView() {
|
||||
return gui.handleVolumeSelect(g, volumesView)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumesNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
panelState := gui.State.Panels.Volumes
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Volumes), false)
|
||||
|
||||
return gui.handleVolumeSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
panelState := gui.State.Panels.Volumes
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Volumes), true)
|
||||
|
||||
return gui.handleVolumeSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumesNextContext(g *gocui.Gui, v *gocui.View) error {
|
||||
contexts := gui.getVolumeContexts()
|
||||
if gui.State.Panels.Volumes.ContextIndex >= len(contexts)-1 {
|
||||
gui.State.Panels.Volumes.ContextIndex = 0
|
||||
} else {
|
||||
gui.State.Panels.Volumes.ContextIndex++
|
||||
}
|
||||
|
||||
gui.handleVolumeSelect(gui.g, v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumesPrevContext(g *gocui.Gui, v *gocui.View) error {
|
||||
contexts := gui.getVolumeContexts()
|
||||
if gui.State.Panels.Volumes.ContextIndex <= 0 {
|
||||
gui.State.Panels.Volumes.ContextIndex = len(contexts) - 1
|
||||
} else {
|
||||
gui.State.Panels.Volumes.ContextIndex--
|
||||
}
|
||||
|
||||
gui.handleVolumeSelect(gui.g, v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type removeVolumeOption struct {
|
||||
description string
|
||||
command string
|
||||
force bool
|
||||
runCommand bool
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *removeVolumeOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
volume, err := gui.getSelectedVolume()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
options := []*removeVolumeOption{
|
||||
{
|
||||
description: gui.Tr.Remove,
|
||||
command: "docker volume rm " + volume.Name,
|
||||
force: false,
|
||||
runCommand: true,
|
||||
},
|
||||
{
|
||||
description: gui.Tr.ForceRemove,
|
||||
command: "docker volume rm --force " + volume.Name,
|
||||
force: true,
|
||||
runCommand: true,
|
||||
},
|
||||
{
|
||||
description: gui.Tr.Cancel,
|
||||
runCommand: false,
|
||||
},
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
if !options[index].runCommand {
|
||||
return nil
|
||||
}
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if cerr := volume.Remove(options[index].force); cerr != nil {
|
||||
return gui.createErrorPanel(gui.g, cerr.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return gui.createMenu("", options, len(options), handleMenuPress)
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePruneVolumes(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||
err := gui.DockerCommand.PruneVolumes()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ type TranslationSet struct {
|
|||
Cancel string
|
||||
CustomCommandTitle string
|
||||
Remove string
|
||||
ForceRemove string
|
||||
RemoveWithVolumes string
|
||||
MustForceToRemoveContainer string
|
||||
Confirm string
|
||||
|
|
@ -43,15 +44,20 @@ type TranslationSet struct {
|
|||
ContainersTitle string
|
||||
StandaloneContainersTitle string
|
||||
ImagesTitle string
|
||||
VolumesTitle string
|
||||
NoContainers string
|
||||
NoContainer string
|
||||
NoImages string
|
||||
NoVolumes string
|
||||
RemoveImage string
|
||||
RemoveVolume string
|
||||
RemoveWithoutPrune string
|
||||
PruneImages string
|
||||
PruneContainers string
|
||||
PruneVolumes string
|
||||
ConfirmPruneContainers string
|
||||
ConfirmPruneImages string
|
||||
ConfirmPruneVolumes string
|
||||
PruningStatus string
|
||||
StopService string
|
||||
PressEnterToReturn string
|
||||
|
|
@ -87,18 +93,21 @@ func englishSet() TranslationSet {
|
|||
EditConfig: "edit config container",
|
||||
Cancel: "cancel",
|
||||
Remove: "remove",
|
||||
ForceRemove: "force remove",
|
||||
RemoveWithVolumes: "remove with volumes",
|
||||
RemoveService: "remove containers",
|
||||
Stop: "stop",
|
||||
Restart: "restart",
|
||||
Rebuild: "rebuild",
|
||||
PreviousContext: "previous context",
|
||||
NextContext: "next context",
|
||||
PreviousContext: "previous tab",
|
||||
NextContext: "next tab",
|
||||
Attach: "attach",
|
||||
ViewLogs: "view logs",
|
||||
RemoveImage: "remove image",
|
||||
RemoveVolume: "remove volume",
|
||||
RemoveWithoutPrune: "remove without deleting untagged parents",
|
||||
PruneContainers: "prune exited containers",
|
||||
PruneVolumes: "prune unused volumes",
|
||||
PruneImages: "prune unused images",
|
||||
ViewRestartOptions: "view restart options",
|
||||
|
||||
|
|
@ -110,6 +119,7 @@ func englishSet() TranslationSet {
|
|||
ContainersTitle: "Containers",
|
||||
StandaloneContainersTitle: "Standalone Containers",
|
||||
ImagesTitle: "Images",
|
||||
VolumesTitle: "Volumes",
|
||||
CustomCommandTitle: "Custom Command:",
|
||||
ErrorTitle: "Error",
|
||||
LogsTitle: "Logs",
|
||||
|
|
@ -121,12 +131,14 @@ func englishSet() TranslationSet {
|
|||
NoContainers: "No containers",
|
||||
NoContainer: "No container",
|
||||
NoImages: "No images",
|
||||
NoVolumes: "No volumes",
|
||||
|
||||
ConfirmQuit: "Are you sure you want to quit?",
|
||||
MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?",
|
||||
NotEnoughSpace: "Not enough space to render panels",
|
||||
ConfirmPruneImages: "Are you sure you want to prune all unused images?",
|
||||
ConfirmPruneContainers: "Are you sure you want to prune all stopped containers?",
|
||||
ConfirmPruneVolumes: "Are you sure you want to prune all unused volumes?",
|
||||
StopService: "Are you sure you want to stop this service's containers? (enter/esc)",
|
||||
StopContainer: "Are you sure you want to stop this container?",
|
||||
PressEnterToReturn: "Press enter to return to lazydocker",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue