add volumes panel

This commit is contained in:
Jesse Duffield 2019-06-10 18:08:23 +10:00
parent 84090bcb94
commit 87c6208577
13 changed files with 519 additions and 80 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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
View 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)
}

View file

@ -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

View file

@ -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)
}()

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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
View 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)
}

View file

@ -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",