mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
Improve projects support
This commit is contained in:
parent
577797d9ed
commit
26ce871368
27 changed files with 415 additions and 128 deletions
0
config/config.yml
Normal file
0
config/config.yml
Normal file
|
|
@ -1,4 +1,3 @@
|
|||
version: '3'
|
||||
services:
|
||||
lazydocker:
|
||||
build:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||
<kbd>[</kbd>: vorheriges Tab
|
||||
<kbd>]</kbd>: nächstes Tab
|
||||
<kbd>/</kbd>: filter list
|
||||
</pre>
|
||||
|
||||
## Container
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: focus main panel
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>/</kbd>: filter list
|
||||
</pre>
|
||||
|
||||
## Containers
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: enfocar panel principal
|
||||
<kbd>[</kbd>: anterior pestaña
|
||||
<kbd>]</kbd>: siguiente pestaña
|
||||
<kbd>/</kbd>: filtrar lista
|
||||
</pre>
|
||||
|
||||
## Contenedores
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: focus panneau principal
|
||||
<kbd>[</kbd>: onglet précédent
|
||||
<kbd>]</kbd>: onglet suivant
|
||||
<kbd>/</kbd>: filter list
|
||||
</pre>
|
||||
|
||||
## Conteneurs
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: focus hoofdpaneel
|
||||
<kbd>[</kbd>: vorige tab
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>/</kbd>: filter list
|
||||
</pre>
|
||||
|
||||
## Containers
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: skup na głównym panelu
|
||||
<kbd>[</kbd>: poprzednia zakładka
|
||||
<kbd>]</kbd>: następna zakładka
|
||||
<kbd>/</kbd>: filter list
|
||||
</pre>
|
||||
|
||||
## Kontenery
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: focar no painel principal
|
||||
<kbd>[</kbd>: aba anterior
|
||||
<kbd>]</kbd>: próxima aba
|
||||
<kbd>/</kbd>: filtrar lista
|
||||
</pre>
|
||||
|
||||
## Contêineres
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: ana panele odaklan
|
||||
<kbd>[</kbd>: önceki sekme
|
||||
<kbd>]</kbd>: sonraki sekme
|
||||
<kbd>/</kbd>: filter list
|
||||
</pre>
|
||||
|
||||
## Konteynerler
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: 聚焦主面板
|
||||
<kbd>[</kbd>: 上一个选项卡
|
||||
<kbd>]</kbd>: 下一个选项卡
|
||||
<kbd>/</kbd>: 过滤列表
|
||||
</pre>
|
||||
|
||||
## 容器
|
||||
|
|
|
|||
4
main.go
4
main.go
|
|
@ -29,6 +29,7 @@ var (
|
|||
configFlag = false
|
||||
debuggingFlag = false
|
||||
composeFiles []string
|
||||
projectName string
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -51,6 +52,7 @@ func main() {
|
|||
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
||||
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
|
||||
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
|
||||
flaggy.String(&projectName, "p", "project", "Specify a docker compose project name")
|
||||
flaggy.SetVersion(info)
|
||||
|
||||
flaggy.Parse()
|
||||
|
|
@ -71,7 +73,7 @@ func main() {
|
|||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir)
|
||||
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func Generate() {
|
|||
}
|
||||
|
||||
func generateAtDir(dir string) {
|
||||
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "")
|
||||
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ func (c *Container) Remove(options container.RemoveOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Start starts the container
|
||||
func (c *Container) Start() error {
|
||||
c.Log.Warn(fmt.Sprintf("starting container %s", c.Name))
|
||||
return c.Client.ContainerStart(context.Background(), c.ID, container.StartOptions{})
|
||||
}
|
||||
|
||||
// Stop stops the container
|
||||
func (c *Container) Stop() error {
|
||||
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
ogLog "log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -39,9 +41,11 @@ type DockerCommand struct {
|
|||
Config *config.AppConfig
|
||||
Client *client.Client
|
||||
InDockerComposeProject bool
|
||||
ErrorChan chan error
|
||||
ContainerMutex deadlock.Mutex
|
||||
ServiceMutex deadlock.Mutex
|
||||
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
|
||||
LocalProjectName string
|
||||
ErrorChan chan error
|
||||
ContainerMutex deadlock.Mutex
|
||||
ServiceMutex deadlock.Mutex
|
||||
|
||||
Closers []io.Closer
|
||||
}
|
||||
|
|
@ -61,12 +65,22 @@ type CommandObject struct {
|
|||
Image *Image
|
||||
Volume *Volume
|
||||
Network *Network
|
||||
Project *Project
|
||||
}
|
||||
|
||||
// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
|
||||
func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||
defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose}
|
||||
_ = mergo.Merge(&defaultObj, obj)
|
||||
|
||||
// When operating on a specific project, include -p flag so that
|
||||
// docker compose targets the correct project.
|
||||
if obj.Service != nil && obj.Service.ProjectName != "" {
|
||||
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Service.ProjectName)
|
||||
} else if obj.Project != nil && obj.Project.Name != "" {
|
||||
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Project.Name)
|
||||
}
|
||||
|
||||
return defaultObj
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +207,7 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
|
|||
container.MonitoringStats = false
|
||||
}
|
||||
|
||||
func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||
c.ServiceMutex.Lock()
|
||||
defer c.ServiceMutex.Unlock()
|
||||
|
||||
|
|
@ -202,27 +216,136 @@ func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service,
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
var services []*Service
|
||||
// we only need to get these services once because they won't change in the runtime of the program
|
||||
if currentServices != nil {
|
||||
services = currentServices
|
||||
} else {
|
||||
services, err = c.GetServices()
|
||||
// Derive services from container labels (covers all projects)
|
||||
services := c.GetServicesFromContainers(containers)
|
||||
|
||||
var composeServices []*Service
|
||||
if c.InDockerComposeProject {
|
||||
composeServices, err = c.GetServices()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
c.Log.Warn("Failed to get compose services: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the local project name before merging services, since
|
||||
// mergeServices needs it. We match compose service names against container
|
||||
// labels to handle cases where the project name differs from the directory
|
||||
// name (e.g. a `name:` directive in the compose file).
|
||||
if c.LocalProjectName == "" && c.InDockerComposeProject && composeServices != nil {
|
||||
for _, ctr := range containers {
|
||||
if ctr.ProjectName == "" || ctr.ServiceName == "" {
|
||||
continue
|
||||
}
|
||||
for _, svc := range composeServices {
|
||||
if ctr.ServiceName == svc.Name {
|
||||
c.LocalProjectName = ctr.ProjectName
|
||||
break
|
||||
}
|
||||
}
|
||||
if c.LocalProjectName != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fall back to directory name
|
||||
if c.LocalProjectName == "" && c.Config.ProjectDir != "" {
|
||||
c.LocalProjectName = filepath.Base(c.Config.ProjectDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge compose services (which include stopped services) with
|
||||
// container-derived services from all projects
|
||||
if composeServices != nil {
|
||||
services = c.mergeServices(services, composeServices)
|
||||
}
|
||||
|
||||
c.assignContainersToServices(containers, services)
|
||||
|
||||
return containers, services, nil
|
||||
}
|
||||
|
||||
// GetServicesFromContainers derives services from container labels for all projects
|
||||
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service {
|
||||
// Use project+service as key to avoid duplicates
|
||||
type serviceKey struct {
|
||||
project string
|
||||
service string
|
||||
}
|
||||
seen := make(map[serviceKey]bool)
|
||||
services := make([]*Service, 0, len(containers))
|
||||
|
||||
for _, ctr := range containers {
|
||||
if ctr.ServiceName == "" || ctr.OneOff {
|
||||
continue
|
||||
}
|
||||
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
services = append(services, &Service{
|
||||
Name: ctr.ServiceName,
|
||||
ID: ctr.ProjectName + "-" + ctr.ServiceName,
|
||||
ProjectName: ctr.ProjectName,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
})
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// mergeServices merges compose services (which may lack ProjectName) with
|
||||
// container-derived services. Compose services take priority because they
|
||||
// include services without running containers.
|
||||
func (c *DockerCommand) mergeServices(containerServices []*Service, composeServices []*Service) []*Service {
|
||||
// Set project name on compose services
|
||||
for _, svc := range composeServices {
|
||||
if svc.ProjectName == "" {
|
||||
svc.ProjectName = c.LocalProjectName
|
||||
}
|
||||
}
|
||||
|
||||
// Build a set of compose service names for the local project
|
||||
composeServiceNames := make(map[string]bool)
|
||||
for _, svc := range composeServices {
|
||||
composeServiceNames[svc.Name] = true
|
||||
}
|
||||
|
||||
// Start with compose services, then add container-derived services
|
||||
// that aren't already covered by compose (i.e. from other projects)
|
||||
result := make([]*Service, 0, len(composeServices)+len(containerServices))
|
||||
result = append(result, composeServices...)
|
||||
|
||||
for _, svc := range containerServices {
|
||||
if svc.ProjectName == c.LocalProjectName && composeServiceNames[svc.Name] {
|
||||
continue // already covered by compose service
|
||||
}
|
||||
result = append(result, svc)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetProjectNames returns all unique project names from containers
|
||||
func (c *DockerCommand) GetProjectNames(containers []*Container) []string {
|
||||
seen := make(map[string]bool)
|
||||
var names []string
|
||||
for _, ctr := range containers {
|
||||
if ctr.ProjectName != "" && !seen[ctr.ProjectName] {
|
||||
seen[ctr.ProjectName] = true
|
||||
names = append(names, ctr.ProjectName)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
||||
L:
|
||||
for _, service := range services {
|
||||
for _, ctr := range containers {
|
||||
if !ctr.OneOff && ctr.ServiceName == service.Name {
|
||||
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
|
||||
service.Container = ctr
|
||||
continue L
|
||||
}
|
||||
|
|
@ -311,7 +434,8 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
|
|||
for i, str := range lines {
|
||||
services[i] = &Service{
|
||||
Name: str,
|
||||
ID: str,
|
||||
ID: c.LocalProjectName + "-" + str,
|
||||
ProjectName: c.LocalProjectName,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
DockerCommand: c,
|
||||
|
|
@ -351,11 +475,11 @@ func (c *DockerCommand) SetContainerDetails(containers []*Container) {
|
|||
}
|
||||
|
||||
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
||||
func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
|
||||
func (c *DockerCommand) ViewAllLogs(project *Project) (*exec.Cmd, error) {
|
||||
cmd := c.OSCommand.ExecutableFromString(
|
||||
utils.ApplyTemplate(
|
||||
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
|
||||
c.NewCommandObject(CommandObject{}),
|
||||
c.NewCommandObject(CommandObject{Project: project}),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -366,10 +490,15 @@ func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
|
|||
|
||||
// DockerComposeConfig returns the result of 'docker-compose config'
|
||||
func (c *DockerCommand) DockerComposeConfig() string {
|
||||
return c.DockerComposeConfigForProject(nil)
|
||||
}
|
||||
|
||||
// DockerComposeConfigForProject returns the result of 'docker-compose config' for a specific project
|
||||
func (c *DockerCommand) DockerComposeConfigForProject(project *Project) string {
|
||||
output, err := c.OSCommand.RunCommandWithOutput(
|
||||
utils.ApplyTemplate(
|
||||
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
|
||||
c.NewCommandObject(CommandObject{}),
|
||||
c.NewCommandObject(CommandObject{Project: project}),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
type Service struct {
|
||||
Name string
|
||||
ID string
|
||||
ProjectName string
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
Container *Container
|
||||
|
|
|
|||
|
|
@ -488,10 +488,11 @@ type AppConfig struct {
|
|||
UserConfig *UserConfig
|
||||
ConfigDir string
|
||||
ProjectDir string
|
||||
ProjectName string
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string) (*AppConfig, error) {
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
|
||||
configDir, err := findOrCreateConfigDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -517,6 +518,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
|||
UserConfig: userConfig,
|
||||
ConfigDir: configDir,
|
||||
ProjectDir: projectDir,
|
||||
ProjectName: projectName,
|
||||
}
|
||||
|
||||
return appConfig, nil
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
func TestDockerComposeCommandNoFiles(t *testing.T) {
|
||||
composeFiles := []string{}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
|
|||
|
||||
func TestDockerComposeCommandSingleFile(t *testing.T) {
|
||||
composeFiles := []string{"one.yml"}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
|
|||
|
||||
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
||||
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
|||
func TestWritingToConfigFile(t *testing.T) {
|
||||
// init the AppConfig
|
||||
emptyComposeFiles := []string{}
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir")
|
||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,11 +175,21 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||
return defaultBox
|
||||
}
|
||||
|
||||
return append([]*boxlayout.Box{
|
||||
{
|
||||
// The project panel is compact (Size: 3) when not focused, but expands
|
||||
// when focused to show the list of projects.
|
||||
projectBox := &boxlayout.Box{
|
||||
Window: sideWindowNames[0],
|
||||
Size: 3,
|
||||
}
|
||||
if currentWindow == sideWindowNames[0] {
|
||||
projectBox = &boxlayout.Box{
|
||||
Window: sideWindowNames[0],
|
||||
Size: 3,
|
||||
},
|
||||
Weight: 2,
|
||||
}
|
||||
}
|
||||
|
||||
return append([]*boxlayout.Box{
|
||||
projectBox,
|
||||
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
||||
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||
})...)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
}
|
||||
|
||||
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool {
|
||||
return service.Name == container.ServiceName
|
||||
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +94,16 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
return false
|
||||
}
|
||||
|
||||
// Filter by selected project. Containers with no project (truly
|
||||
// standalone, not from any compose project) are always shown.
|
||||
selectedProject := gui.getSelectedProjectName()
|
||||
if selectedProject == "" {
|
||||
selectedProject = gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
if selectedProject != "" && container.ProjectName != "" && container.ProjectName != selectedProject {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
GetTableCells: func(container *commands.Container) []string {
|
||||
|
|
@ -256,7 +266,6 @@ func (gui *Gui) refreshContainersAndServices() error {
|
|||
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
|
||||
|
||||
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
|
||||
gui.Panels.Services.List.GetAllItems(),
|
||||
gui.Panels.Containers.List.GetAllItems(),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -283,10 +292,8 @@ func (gui *Gui) refreshContainersAndServices() error {
|
|||
}
|
||||
|
||||
func (gui *Gui) renderContainersAndServices() error {
|
||||
if gui.DockerCommand.InDockerComposeProject {
|
||||
if err := gui.Panels.Services.RerenderList(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.Panels.Services.RerenderList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.Panels.Containers.RerenderList(); err != nil {
|
||||
|
|
|
|||
|
|
@ -297,12 +297,12 @@ func (gui *Gui) updateContainerDetails() error {
|
|||
|
||||
func (gui *Gui) refresh() {
|
||||
go func() {
|
||||
if err := gui.refreshProject(); err != nil {
|
||||
// Refresh containers/services first, then projects (which depend on
|
||||
// container labels to discover projects).
|
||||
if err := gui.refreshContainersAndServices(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if err := gui.refreshContainersAndServices(); err != nil {
|
||||
if err := gui.refreshProject(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -120,9 +120,11 @@ func (gui *Gui) focusPointInView(view *gocui.View) {
|
|||
return
|
||||
}
|
||||
|
||||
currentPanel, ok := gui.currentListPanel()
|
||||
if ok {
|
||||
currentPanel.Refocus()
|
||||
for _, panel := range gui.allListPanels() {
|
||||
if panel.GetView() == view {
|
||||
panel.Refocus()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ type SideListPanel[T comparable] struct {
|
|||
// a callback to invoke when the item is clicked
|
||||
OnClick func(T) error
|
||||
|
||||
// a callback to invoke when a new item is selected (e.g. keyboard navigation)
|
||||
OnSelect func(T) error
|
||||
|
||||
// returns the cells that we render to the view in a table format. The cells will
|
||||
// be rendered with padding.
|
||||
GetTableCells func(T) []string
|
||||
|
|
@ -116,6 +119,12 @@ func (self *SideListPanel[T]) HandleSelect() error {
|
|||
|
||||
self.Refocus()
|
||||
|
||||
if self.OnSelect != nil {
|
||||
if err := self.OnSelect(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return self.renderContext(item)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package gui
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
|
@ -16,34 +15,21 @@ import (
|
|||
"github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
// Although at the moment we'll only have one project, in future we could have
|
||||
// a list of projects in the project panel.
|
||||
|
||||
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
||||
return &panels.SideListPanel[*commands.Project]{
|
||||
ContextState: &panels.ContextState[*commands.Project]{
|
||||
GetMainTabs: func() []panels.MainTab[*commands.Project] {
|
||||
if gui.DockerCommand.InDockerComposeProject {
|
||||
return []panels.MainTab[*commands.Project]{
|
||||
{
|
||||
Key: "logs",
|
||||
Title: gui.Tr.LogsTitle,
|
||||
Render: gui.renderAllLogs,
|
||||
},
|
||||
{
|
||||
Key: "config",
|
||||
Title: gui.Tr.DockerComposeConfigTitle,
|
||||
Render: gui.renderDockerComposeConfig,
|
||||
},
|
||||
{
|
||||
Key: "credits",
|
||||
Title: gui.Tr.CreditsTitle,
|
||||
Render: gui.renderCredits,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []panels.MainTab[*commands.Project]{
|
||||
{
|
||||
Key: "logs",
|
||||
Title: gui.Tr.LogsTitle,
|
||||
Render: gui.renderAllLogs,
|
||||
},
|
||||
{
|
||||
Key: "config",
|
||||
Title: gui.Tr.DockerComposeConfigTitle,
|
||||
Render: gui.renderDockerComposeConfig,
|
||||
},
|
||||
{
|
||||
Key: "credits",
|
||||
Title: gui.Tr.CreditsTitle,
|
||||
|
|
@ -64,31 +50,87 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
|||
Gui: gui.intoInterface(),
|
||||
|
||||
Sort: func(a *commands.Project, b *commands.Project) bool {
|
||||
return false
|
||||
return a.Name < b.Name
|
||||
},
|
||||
GetTableCells: presentation.GetProjectDisplayStrings,
|
||||
// It doesn't make sense to filter a list of only one item.
|
||||
DisableFilter: true,
|
||||
OnSelect: func(project *commands.Project) error {
|
||||
// When a different project is selected, re-filter services and
|
||||
// containers to show only those belonging to the selected project.
|
||||
return gui.renderContainersAndServices()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshProject() error {
|
||||
gui.Panels.Projects.SetItems([]*commands.Project{{Name: gui.getProjectName()}})
|
||||
return gui.Panels.Projects.RerenderList()
|
||||
}
|
||||
projects := gui.getDiscoveredProjects()
|
||||
|
||||
func (gui *Gui) getProjectName() string {
|
||||
projectName := path.Base(gui.Config.ProjectDir)
|
||||
if gui.DockerCommand.InDockerComposeProject {
|
||||
for _, service := range gui.Panels.Services.List.GetAllItems() {
|
||||
container := service.Container
|
||||
if container != nil && container.DetailsLoaded() {
|
||||
return container.Details.Config.Labels["com.docker.compose.project"]
|
||||
// Preserve the current selection across refreshes. On the first refresh,
|
||||
// select the project specified via -p flag, or fall back to the local project.
|
||||
selectedName := gui.getSelectedProjectName()
|
||||
if selectedName == "" {
|
||||
if gui.Config.ProjectName != "" {
|
||||
selectedName = gui.Config.ProjectName
|
||||
} else {
|
||||
selectedName = gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
}
|
||||
|
||||
gui.Panels.Projects.SetItems(projects)
|
||||
|
||||
if selectedName != "" {
|
||||
for i, p := range gui.Panels.Projects.List.GetItems() {
|
||||
if p.Name == selectedName {
|
||||
gui.Panels.Projects.SetSelectedLineIdx(i)
|
||||
gui.Panels.Projects.Refocus()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return projectName
|
||||
return gui.Panels.Projects.RerenderList()
|
||||
}
|
||||
|
||||
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
||||
// The local project (from docker-compose.yml in the current directory) is included if
|
||||
// it has running containers or if InDockerComposeProject is true.
|
||||
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
|
||||
containers := gui.Panels.Containers.List.GetAllItems()
|
||||
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
||||
|
||||
// If we're in a docker compose project but it has no running containers,
|
||||
// still include it. We don't fall back to the directory name here to avoid
|
||||
// briefly flashing the wrong project name on startup.
|
||||
localName := gui.DockerCommand.LocalProjectName
|
||||
|
||||
if gui.DockerCommand.InDockerComposeProject && localName != "" {
|
||||
found := false
|
||||
for _, name := range projectNames {
|
||||
if name == localName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
projectNames = append([]string{localName}, projectNames...)
|
||||
}
|
||||
}
|
||||
|
||||
projects := make([]*commands.Project, len(projectNames))
|
||||
for i, name := range projectNames {
|
||||
projects[i] = &commands.Project{Name: name}
|
||||
}
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
// getSelectedProjectName returns the name of the currently selected project,
|
||||
// or empty string if none is selected.
|
||||
func (gui *Gui) getSelectedProjectName() string {
|
||||
project, err := gui.Panels.Projects.GetSelectedItem()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return project.Name
|
||||
}
|
||||
|
||||
func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc {
|
||||
|
|
@ -112,7 +154,7 @@ func (gui *Gui) creditsStr() string {
|
|||
}, "\n\n")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
|
||||
func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
|
||||
return gui.NewTask(TaskOpts{
|
||||
Autoscroll: true,
|
||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||
|
|
@ -122,7 +164,7 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
|
|||
cmd := gui.OSCommand.RunCustomCommand(
|
||||
utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.AllLogs,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -144,9 +186,14 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
|
|||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) tasks.TaskFunc {
|
||||
func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc {
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return "Compose config is not available for non-local projects"
|
||||
})
|
||||
}
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfig())
|
||||
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfigForProject(project))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +220,8 @@ func lazydockerTitle() string {
|
|||
|
||||
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
|
||||
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
|
||||
c, err := gui.DockerCommand.ViewAllLogs()
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
c, err := gui.DockerCommand.ViewAllLogs(project)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,11 +74,24 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
|
|||
|
||||
return a.Name < b.Name
|
||||
},
|
||||
Filter: func(service *commands.Service) bool {
|
||||
selectedProject := gui.getSelectedProjectName()
|
||||
if selectedProject == "" {
|
||||
// Before any project is selected (e.g. startup), default to
|
||||
// the local project so we don't briefly flash all services.
|
||||
selectedProject = gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
if selectedProject == "" {
|
||||
return true
|
||||
}
|
||||
return service.ProjectName == selectedProject
|
||||
},
|
||||
GetTableCells: func(service *commands.Service) []string {
|
||||
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
|
||||
},
|
||||
Hide: func() bool {
|
||||
return !gui.DockerCommand.InDockerComposeProject
|
||||
// Show services panel if there are any compose projects (local or discovered)
|
||||
return !gui.DockerCommand.InDockerComposeProject && len(gui.Panels.Services.List.GetAllItems()) == 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -142,13 +155,24 @@ func (r *commandOption) getDisplayStrings() []string {
|
|||
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
|
||||
}
|
||||
|
||||
// isServiceFromLocalProject returns true if the given service belongs to the
|
||||
// local compose project (the one whose compose file is in the current directory).
|
||||
// Compose commands like up/stop/restart only work for local project services.
|
||||
func (gui *Gui) isServiceFromLocalProject(service *commands.Service) bool {
|
||||
return service.ProjectName == gui.DockerCommand.LocalProjectName
|
||||
}
|
||||
|
||||
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
service, err := gui.Panels.Services.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
|
||||
composeCommand := gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}).DockerCompose
|
||||
|
||||
options := []*commandOption{
|
||||
{
|
||||
|
|
@ -202,10 +226,15 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
|
|||
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return service.Container.Stop()
|
||||
}
|
||||
if err := service.Stop(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
|
|
@ -217,6 +246,10 @@ func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
|
||||
if err := service.Up(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
|
|
@ -233,10 +266,15 @@ func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return service.Container.Restart()
|
||||
}
|
||||
if err := service.Restart(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -247,11 +285,19 @@ func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
if service.Container == nil {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
||||
return service.Container.Start()
|
||||
})
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
||||
if err := service.Start(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -289,10 +335,14 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
|
|||
}
|
||||
|
||||
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
|
||||
cmdStr := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.Up,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
)
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
||||
|
|
@ -305,14 +355,18 @@ func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
downCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.Down,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
)
|
||||
|
||||
downWithVolumesCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||
)
|
||||
|
||||
options := []*commandOption{
|
||||
|
|
@ -361,6 +415,10 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if !gui.isServiceFromLocalProject(service) {
|
||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||
}
|
||||
|
||||
rebuildCommand := utils.ApplyTemplate(
|
||||
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||
|
|
@ -465,8 +523,9 @@ L:
|
|||
}
|
||||
|
||||
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project})
|
||||
|
||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ func (gui *Gui) createAllViews() error {
|
|||
|
||||
gui.Views.Project.Title = gui.Tr.ProjectTitle
|
||||
gui.Views.Project.TitlePrefix = "[1]"
|
||||
gui.Views.Project.Highlight = true
|
||||
gui.Views.Project.SelBgColor = selectedLineBgColor
|
||||
|
||||
gui.Views.Services.Highlight = true
|
||||
gui.Views.Services.Title = gui.Tr.ServicesTitle
|
||||
|
|
|
|||
|
|
@ -110,17 +110,18 @@ type TranslationSet struct {
|
|||
OpenInBrowser string
|
||||
SortContainersByState string
|
||||
|
||||
LogsTitle string
|
||||
ConfigTitle string
|
||||
EnvTitle string
|
||||
DockerComposeConfigTitle string
|
||||
StatsTitle string
|
||||
CreditsTitle string
|
||||
ContainerConfigTitle string
|
||||
ContainerEnvTitle string
|
||||
NothingToDisplay string
|
||||
NoContainerForService string
|
||||
CannotDisplayEnvVariables string
|
||||
LogsTitle string
|
||||
ConfigTitle string
|
||||
EnvTitle string
|
||||
DockerComposeConfigTitle string
|
||||
StatsTitle string
|
||||
CreditsTitle string
|
||||
ContainerConfigTitle string
|
||||
ContainerEnvTitle string
|
||||
NothingToDisplay string
|
||||
NoContainerForService string
|
||||
CannotDisplayEnvVariables string
|
||||
CannotManageNonLocalService string
|
||||
|
||||
No string
|
||||
Yes string
|
||||
|
|
@ -217,30 +218,31 @@ func englishSet() TranslationSet {
|
|||
OpenInBrowser: "open in browser (first port is http)",
|
||||
SortContainersByState: "sort containers by state",
|
||||
|
||||
GlobalTitle: "Global",
|
||||
MainTitle: "Main",
|
||||
ProjectTitle: "Project",
|
||||
ServicesTitle: "Services",
|
||||
ContainersTitle: "Containers",
|
||||
StandaloneContainersTitle: "Standalone Containers",
|
||||
ImagesTitle: "Images",
|
||||
VolumesTitle: "Volumes",
|
||||
NetworksTitle: "Networks",
|
||||
CustomCommandTitle: "Custom Command:",
|
||||
BulkCommandTitle: "Bulk Command:",
|
||||
ErrorTitle: "Error",
|
||||
LogsTitle: "Logs",
|
||||
ConfigTitle: "Config",
|
||||
EnvTitle: "Env",
|
||||
DockerComposeConfigTitle: "Docker-Compose Config",
|
||||
TopTitle: "Top",
|
||||
StatsTitle: "Stats",
|
||||
CreditsTitle: "About",
|
||||
ContainerConfigTitle: "Container Config",
|
||||
ContainerEnvTitle: "Container Env",
|
||||
NothingToDisplay: "Nothing to display",
|
||||
NoContainerForService: "No logs to show; service is not associated with a container",
|
||||
CannotDisplayEnvVariables: "Something went wrong while displaying environment variables",
|
||||
GlobalTitle: "Global",
|
||||
MainTitle: "Main",
|
||||
ProjectTitle: "Project",
|
||||
ServicesTitle: "Services",
|
||||
ContainersTitle: "Containers",
|
||||
StandaloneContainersTitle: "Standalone Containers",
|
||||
ImagesTitle: "Images",
|
||||
VolumesTitle: "Volumes",
|
||||
NetworksTitle: "Networks",
|
||||
CustomCommandTitle: "Custom Command:",
|
||||
BulkCommandTitle: "Bulk Command:",
|
||||
ErrorTitle: "Error",
|
||||
LogsTitle: "Logs",
|
||||
ConfigTitle: "Config",
|
||||
EnvTitle: "Env",
|
||||
DockerComposeConfigTitle: "Docker-Compose Config",
|
||||
TopTitle: "Top",
|
||||
StatsTitle: "Stats",
|
||||
CreditsTitle: "About",
|
||||
ContainerConfigTitle: "Container Config",
|
||||
ContainerEnvTitle: "Container Env",
|
||||
NothingToDisplay: "Nothing to display",
|
||||
NoContainerForService: "No logs to show; service is not associated with a container",
|
||||
CannotDisplayEnvVariables: "Something went wrong while displaying environment variables",
|
||||
CannotManageNonLocalService: "This service belongs to a different compose project. Run lazydocker from that project's directory to manage it.",
|
||||
|
||||
NoContainers: "No containers",
|
||||
NoContainer: "No container",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue