Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
2ed3dc36f3 | |||
e6c10bac60 | |||
5a627637f4 | |||
697e66f9c4 | |||
eea2097b73 | |||
5c5f2007f4 | |||
6f8a984f3c | |||
c23090739a | |||
8705777203 | |||
86730024f6 | |||
49a438bbb8 | |||
4d0b2a7a8c | |||
bc2216a9f8 | |||
89f34d8621 | |||
90fe6b862e | |||
dcc8edd3f8 | |||
e9c7c12450 | |||
![]() |
a1e3c7202e | ||
![]() |
774a38d236 | ||
![]() |
d56326304d | ||
![]() |
549ca648df | ||
![]() |
1963d02b56 | ||
![]() |
eb4f0a0568 | ||
![]() |
813652e881 | ||
![]() |
e1543cc0e5 | ||
![]() |
66318e645e | ||
![]() |
311db6f32e | ||
![]() |
60b7425070 | ||
![]() |
78503982b8 | ||
![]() |
df9c97a4ab | ||
![]() |
cad5588b72 | ||
![]() |
13d16fac31 | ||
![]() |
5f7db80c51 | ||
![]() |
cac5fe0e3d |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1 @@
|
||||
/.idea
|
||||
/venv
|
||||
qocker-miqt
|
||||
|
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal file
@ -0,0 +1,14 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.3.0 2025-01-06
|
||||
|
||||
- Port Qocker to MIQT
|
||||
- Add `--sudo` argument support
|
||||
|
||||
## v0.2 2024-09-26
|
||||
|
||||
- Upstream v0.2
|
||||
|
||||
## v0.1 2024-09-25
|
||||
|
||||
- Upstream v0.1
|
68
README.md
68
README.md
@ -1,63 +1,17 @@
|
||||
# Qocker
|
||||
# Qocker-miqt
|
||||
|
||||

|
||||
Qocker-miqt is a user-friendly GUI application for managing Docker containers.
|
||||
|
||||
Qocker is a user-friendly GUI application for managing Docker containers. Built with PyQt5, it provides an intuitive interface for viewing and interacting with your Docker containers.
|
||||
This is a fork of [Qocker](https://github.com/xlmnxp/Qocker) ported to the [MIQT](https://github.com/mappu/miqt) library for demonstration purposes.
|
||||
|
||||
## Features
|
||||
## Building
|
||||
|
||||
- **Container Overview**: View all your Docker containers in a tree-like structure.
|
||||
- **Quick Terminal Access**: Open a terminal for any container with a double-click.
|
||||
- **Container Management**: Start, stop, and remove containers directly from the GUI.
|
||||
- **Real-time Updates**: Container statuses are updated in real-time.
|
||||
- **Cross-platform**: Works on Windows, macOS, and Linux.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Ensure you have Python 3.6+ and Docker installed on your system.
|
||||
2. Clone this repository:
|
||||
```
|
||||
git clone https://github.com/xlmnxp/qocker.git
|
||||
```bash
|
||||
apt install qt6-base-dev build-essential golang-go
|
||||
go build -ldflags '-s -w'
|
||||
./qocker-miqt
|
||||
```
|
||||
|
||||
3. Navigate to the project directory:
|
||||
```
|
||||
cd qocker
|
||||
```
|
||||
4. Install the required dependencies:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To start Qocker, run:
|
||||
```
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
- **View Containers**: All your Docker containers will be displayed in the main window.
|
||||
- **Open Terminal**: Double-click on any container to open a terminal session for that container.
|
||||
- **Manage Containers**: Use the buttons or context menu to start, stop, or remove containers.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6+
|
||||
- PyQt5
|
||||
- Docker
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to Qocker are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE.md) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Thanks to the PyQt and Docker teams for their fantastic tools.
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any questions, feel free to reach out to me at [email](mailto:s@sy.sa).
|
||||
If your `docker` binary requires `sudo`, then
|
||||
1. Run `sudo docker` once to prime the sudo login cache; then
|
||||
2. Run `./qocker-miqt --sudo`. Then qocker will use sudo for all docker invocations.
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 146 KiB |
BIN
doc/screenshot.png
Normal file
BIN
doc/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module code.ivysaur.me/qocker-miqt
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
github.com/mappu/miqt v0.7.1 h1:CIegOqnF9sxSHs4eyqOgAHbuhFwCu1hth4b989ZTP1k=
|
||||
github.com/mappu/miqt v0.7.1/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
|
||||
github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c h1:4oJzer4B//aJ8B/dCFF9yLPgSLobqXVhUpfQh/Tdc5U=
|
||||
github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
|
920
main.go
Normal file
920
main.go
Normal file
@ -0,0 +1,920 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
qt "github.com/mappu/miqt/qt6"
|
||||
"github.com/mappu/miqt/qt6/mainthread"
|
||||
)
|
||||
|
||||
const AutoRefreshInterval = 1 * time.Second
|
||||
|
||||
const (
|
||||
ContainersTab int = 0
|
||||
ImagesTab int = 1
|
||||
NetworksTab int = 2
|
||||
VolumesTab int = 3
|
||||
)
|
||||
|
||||
var (
|
||||
dockerSudo bool
|
||||
)
|
||||
|
||||
func NewStatusDelegate(status string) *qt.QWidget {
|
||||
mw := qt.NewQWidget2()
|
||||
|
||||
layout := qt.NewQHBoxLayout(mw)
|
||||
layout.SetContentsMargins(4, 4, 4, 4)
|
||||
layout.SetSpacing(8)
|
||||
|
||||
statusCircle := qt.NewQWidget2()
|
||||
statusCircle.SetFixedSize2(12, 12)
|
||||
|
||||
var color string = "red"
|
||||
if strings.Contains(status, "Up") {
|
||||
color = "green"
|
||||
}
|
||||
statusCircle.SetStyleSheet("background-color: " + color + "; border-radius: 6px;")
|
||||
|
||||
statusLabel := qt.NewQLabel3(status)
|
||||
|
||||
layout.AddWidget(statusCircle)
|
||||
layout.AddWidget(statusLabel.QWidget)
|
||||
layout.AddStretch()
|
||||
|
||||
return mw
|
||||
}
|
||||
|
||||
func dockerCommand(args ...string) *exec.Cmd {
|
||||
if dockerSudo {
|
||||
newargs := []string{"docker"}
|
||||
newargs = append(newargs, args...)
|
||||
return exec.Command("sudo", newargs...)
|
||||
}
|
||||
|
||||
return dockerCommand(args...)
|
||||
}
|
||||
|
||||
func openTerminal(container_id string) {
|
||||
command := "docker exec -it " + container_id + " sh -c '[ -x /bin/bash ] && exec /bin/bash || exec /bin/sh'"
|
||||
if dockerSudo {
|
||||
command = "sudo " + command
|
||||
}
|
||||
|
||||
popupCommand(command)
|
||||
}
|
||||
|
||||
func openLogs(container_id string) {
|
||||
command := "docker logs -f " + container_id
|
||||
if dockerSudo {
|
||||
command = "sudo " + command
|
||||
}
|
||||
|
||||
popupCommand(command)
|
||||
}
|
||||
|
||||
func popupCommand(command string) {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
exec.Command("open", "-a", "Terminal", "--", "sh", "-c", command).Start()
|
||||
|
||||
case "linux":
|
||||
exec.Command("x-terminal-emulator", "-e", "sh -c \""+command+"\"").Start()
|
||||
|
||||
case "windows":
|
||||
exec.Command("start", "cmd", "/k", command).Start()
|
||||
|
||||
default:
|
||||
panic("Opening a terminal is not supported on " + runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
type DockerGUITab struct {
|
||||
*qt.QWidget
|
||||
|
||||
tree *qt.QTreeWidget
|
||||
search_bar *qt.QLineEdit
|
||||
}
|
||||
|
||||
type DockerGUI struct {
|
||||
*qt.QMainWindow
|
||||
|
||||
toolbar *qt.QToolBar
|
||||
|
||||
start_action *qt.QAction
|
||||
stop_action *qt.QAction
|
||||
remove_action *qt.QAction
|
||||
create_network_action *qt.QAction
|
||||
remove_network_action *qt.QAction
|
||||
create_volume_action *qt.QAction
|
||||
remove_volume_action *qt.QAction
|
||||
terminal_action *qt.QAction
|
||||
pull_image_action *qt.QAction
|
||||
remove_image_action *qt.QAction
|
||||
logs_action *qt.QAction
|
||||
|
||||
auto_refresh_checkbox *qt.QCheckBox
|
||||
|
||||
tab_widget *qt.QTabWidget
|
||||
|
||||
containers_tab *qt.QWidget
|
||||
images_tab *qt.QWidget
|
||||
networks_tab *qt.QWidget
|
||||
volumes_tab *qt.QWidget
|
||||
|
||||
containers_tree *qt.QTreeWidget
|
||||
images_tree *qt.QTreeWidget
|
||||
networks_tree *qt.QTreeWidget
|
||||
volumes_tree *qt.QTreeWidget
|
||||
|
||||
containers_searchbar *qt.QLineEdit
|
||||
images_searchbar *qt.QLineEdit
|
||||
networks_searchbar *qt.QLineEdit
|
||||
volumes_searchbar *qt.QLineEdit
|
||||
|
||||
refresh_timer *time.Ticker
|
||||
}
|
||||
|
||||
func NewDockerGUI() *DockerGUI {
|
||||
var self DockerGUI
|
||||
|
||||
self.QMainWindow = qt.NewQMainWindow2()
|
||||
|
||||
self.SetWindowTitle("Qocker - Docker Graphical User Interface")
|
||||
self.SetGeometry(100, 100, 1000, 600)
|
||||
|
||||
// Create central widget and layout
|
||||
central_widget := qt.NewQWidget2()
|
||||
self.SetCentralWidget(central_widget)
|
||||
mainLayout := qt.NewQVBoxLayout(central_widget)
|
||||
|
||||
// Create toolbar
|
||||
self.create_toolbar()
|
||||
|
||||
// Create tab widget
|
||||
self.tab_widget = qt.NewQTabWidget2()
|
||||
mainLayout.AddWidget(self.tab_widget.QWidget)
|
||||
|
||||
// Create tabs
|
||||
self.containers_tab = qt.NewQWidget2()
|
||||
self.images_tab = qt.NewQWidget2()
|
||||
self.networks_tab = qt.NewQWidget2()
|
||||
self.volumes_tab = qt.NewQWidget2()
|
||||
|
||||
self.tab_widget.AddTab(self.containers_tab, "Containers")
|
||||
self.tab_widget.AddTab(self.images_tab, "Images")
|
||||
self.tab_widget.AddTab(self.networks_tab, "Networks")
|
||||
self.tab_widget.AddTab(self.volumes_tab, "Volumes")
|
||||
|
||||
// Connect tab change to toolbar update
|
||||
self.tab_widget.OnCurrentChanged(self.update_toolbar_buttons)
|
||||
|
||||
// Create tree widgets for each tab
|
||||
self.containers_tree = self.create_tree_widget("ID", "Name", "Image", "Status", "Ports")
|
||||
self.images_tree = self.create_tree_widget("ID", "Repository", "Tag", "Size")
|
||||
self.networks_tree = self.create_tree_widget("ID", "Name", "Driver")
|
||||
self.volumes_tree = self.create_tree_widget("Name", "Driver", "Mountpoint")
|
||||
|
||||
self.containers_tree.OnItemDoubleClicked(func(item *qt.QTreeWidgetItem, column int) {
|
||||
self.open_terminal()
|
||||
})
|
||||
|
||||
// Add search bars
|
||||
self.containers_searchbar = self.create_searchbar_widget(self.containers_tree, "Search containers...")
|
||||
self.images_searchbar = self.create_searchbar_widget(self.images_tree, "Search images...")
|
||||
self.networks_searchbar = self.create_searchbar_widget(self.networks_tree, "Search networks...")
|
||||
self.volumes_searchbar = self.create_searchbar_widget(self.volumes_tree, "Search volumes...")
|
||||
|
||||
// Add tree widgets to tabs
|
||||
self.setup_tab(self.containers_tab, self.containers_tree, self.containers_searchbar)
|
||||
self.setup_tab(self.images_tab, self.images_tree, self.images_searchbar)
|
||||
self.setup_tab(self.networks_tab, self.networks_tree, self.networks_searchbar)
|
||||
self.setup_tab(self.volumes_tab, self.volumes_tree, self.volumes_searchbar)
|
||||
|
||||
// Create menu bar
|
||||
self.create_menu_bar()
|
||||
|
||||
//.Setup auto-refresh
|
||||
self.setup_auto_refresh()
|
||||
|
||||
// Populate data
|
||||
self.refresh_data()
|
||||
|
||||
// Update toolbar buttons for initial state
|
||||
self.update_toolbar_buttons(0)
|
||||
|
||||
// override QMainWindow.createPopupMenu
|
||||
self.OnCreatePopupMenu(func(super func() *qt.QMenu) *qt.QMenu {
|
||||
filtered_menu := super()
|
||||
filtered_menu.RemoveAction(self.toolbar.ToggleViewAction())
|
||||
|
||||
return filtered_menu
|
||||
})
|
||||
|
||||
return &self
|
||||
}
|
||||
|
||||
func (self *DockerGUI) create_tree_widget(headers ...string) *qt.QTreeWidget {
|
||||
tree := qt.NewQTreeWidget2()
|
||||
tree.SetHeaderLabels(headers)
|
||||
tree.SetContextMenuPolicy(qt.CustomContextMenu)
|
||||
tree.OnCustomContextMenuRequested(self.show_context_menu)
|
||||
tree.SetIndentation(0)
|
||||
tree.SetUniformRowHeights(true) // Speedup
|
||||
tree.SetSortingEnabled(true)
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
// Add search bar
|
||||
func (self *DockerGUI) create_searchbar_widget(tree *qt.QTreeWidget, search_placeholder string) *qt.QLineEdit {
|
||||
|
||||
search_bar := qt.NewQLineEdit2()
|
||||
search_bar.SetPlaceholderText(search_placeholder)
|
||||
search_bar.OnTextChanged(func(text string) {
|
||||
self.filter_tree(tree, text)
|
||||
})
|
||||
|
||||
return search_bar
|
||||
}
|
||||
|
||||
func (self *DockerGUI) setup_tab(tab *qt.QWidget, tree *qt.QTreeWidget, search_bar *qt.QLineEdit) {
|
||||
layout := qt.NewQVBoxLayout(tab)
|
||||
layout.AddWidget(search_bar.QWidget)
|
||||
layout.AddWidget(tree.QWidget)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) filter_tree(tree *qt.QTreeWidget, text string) {
|
||||
text = strings.ToLower(text)
|
||||
|
||||
for i := 0; i < tree.TopLevelItemCount(); i++ {
|
||||
item := tree.TopLevelItem(i)
|
||||
|
||||
match := false
|
||||
for j := 0; j < item.ColumnCount(); j++ {
|
||||
if strings.Contains(strings.ToLower(item.Text(j)), text) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
item.SetHidden(!match)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DockerGUI) create_toolbar() {
|
||||
self.toolbar = qt.NewQToolBar3()
|
||||
self.toolbar.SetMovable(false) // Make toolbar fixed
|
||||
self.AddToolBar(qt.TopToolBarArea, self.toolbar)
|
||||
|
||||
// Common actions
|
||||
refresh_action := qt.NewQAction6(qt.QIcon_FromTheme("view-refresh"), "Refresh", self.QObject)
|
||||
refresh_action.OnTriggered(self.refresh_data)
|
||||
self.toolbar.AddAction(refresh_action)
|
||||
|
||||
// Add auto-refresh checkbox
|
||||
self.auto_refresh_checkbox = qt.NewQCheckBox3("Auto-refresh")
|
||||
self.auto_refresh_checkbox.SetChecked(true)
|
||||
self.toolbar.AddWidget(self.auto_refresh_checkbox.QWidget)
|
||||
|
||||
// Add separator
|
||||
self.toolbar.AddSeparator()
|
||||
|
||||
// Container-specific actions
|
||||
self.start_action = qt.NewQAction6(qt.QIcon_FromTheme("media-playback-start"), "Start", self.QObject)
|
||||
self.start_action.OnTriggered(self.start_container)
|
||||
self.toolbar.AddAction(self.start_action)
|
||||
|
||||
self.stop_action = qt.NewQAction6(qt.QIcon_FromTheme("media-playback-stop"), "Stop", self.QObject)
|
||||
self.stop_action.OnTriggered(self.stop_container)
|
||||
self.toolbar.AddAction(self.stop_action)
|
||||
|
||||
self.remove_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove", self.QObject)
|
||||
self.remove_action.OnTriggered(self.remove_container)
|
||||
self.toolbar.AddAction(self.remove_action)
|
||||
|
||||
// Image-specific actions
|
||||
self.pull_image_action = qt.NewQAction6(qt.QIcon_FromTheme("download"), "Pull Image", self.QObject)
|
||||
self.pull_image_action.OnTriggered(self.pull_image)
|
||||
self.toolbar.AddAction(self.pull_image_action)
|
||||
|
||||
self.remove_image_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove Image", self.QObject)
|
||||
self.remove_image_action.OnTriggered(self.remove_image)
|
||||
self.toolbar.AddAction(self.remove_image_action)
|
||||
|
||||
// Network-specific actions
|
||||
self.create_network_action = qt.NewQAction6(qt.QIcon_FromTheme("list-add"), "Create Network", self.QObject)
|
||||
self.create_network_action.OnTriggered(self.create_network)
|
||||
self.toolbar.AddAction(self.create_network_action)
|
||||
|
||||
self.remove_network_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove Network", self.QObject)
|
||||
self.remove_network_action.OnTriggered(self.remove_network)
|
||||
self.toolbar.AddAction(self.remove_network_action)
|
||||
|
||||
// Volume-specific actions
|
||||
self.create_volume_action = qt.NewQAction6(qt.QIcon_FromTheme("list-add"), "Create Volume", self.QObject)
|
||||
self.create_volume_action.OnTriggered(self.create_volume)
|
||||
self.toolbar.AddAction(self.create_volume_action)
|
||||
|
||||
self.remove_volume_action = qt.NewQAction6(qt.QIcon_FromTheme("edit-delete"), "Remove Volume", self.QObject)
|
||||
self.remove_volume_action.OnTriggered(self.remove_volume)
|
||||
self.toolbar.AddAction(self.remove_volume_action)
|
||||
|
||||
// Add terminal action
|
||||
self.terminal_action = qt.NewQAction6(qt.QIcon_FromTheme("utilities-terminal"), "Open Terminal", self.QObject)
|
||||
self.terminal_action.OnTriggered(self.open_terminal)
|
||||
self.toolbar.AddAction(self.terminal_action)
|
||||
|
||||
// Add logs action
|
||||
self.logs_action = qt.NewQAction6(qt.QIcon_FromTheme("document-open"), "Open Logs", self.QObject)
|
||||
self.logs_action.OnTriggered(self.open_logs)
|
||||
self.toolbar.AddAction(self.logs_action)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) update_toolbar_buttons(index int) {
|
||||
// Show actions based on the current tab
|
||||
|
||||
// Containers tab
|
||||
self.start_action.SetVisible(index == ContainersTab)
|
||||
self.stop_action.SetVisible(index == ContainersTab)
|
||||
self.remove_action.SetVisible(index == ContainersTab)
|
||||
self.terminal_action.SetVisible(index == ContainersTab)
|
||||
self.logs_action.SetVisible(index == ContainersTab)
|
||||
|
||||
// Images tab
|
||||
self.pull_image_action.SetVisible(index == ImagesTab)
|
||||
self.remove_image_action.SetVisible(index == ImagesTab)
|
||||
|
||||
// Networks tab
|
||||
self.create_network_action.SetVisible(index == NetworksTab)
|
||||
self.remove_network_action.SetVisible(index == NetworksTab)
|
||||
|
||||
// Volumes tab
|
||||
self.create_volume_action.SetVisible(index == VolumesTab)
|
||||
self.remove_volume_action.SetVisible(index == VolumesTab)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) create_menu_bar() {
|
||||
menubar := self.MenuBar()
|
||||
|
||||
file_menu := menubar.AddMenuWithTitle("&File")
|
||||
exit_action := qt.NewQAction5("E&xit", self.QObject)
|
||||
exit_action.OnTriggered(func() {
|
||||
self.Close()
|
||||
})
|
||||
file_menu.AddAction(exit_action)
|
||||
|
||||
docker_menu := menubar.AddMenuWithTitle("&Docker")
|
||||
refresh_action := qt.NewQAction5("&Refresh", self.QObject)
|
||||
refresh_action.OnTriggered(self.refresh_data)
|
||||
docker_menu.AddAction(refresh_action)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) show_context_menu(position *qt.QPoint) {
|
||||
context_menu := qt.NewQMenu2()
|
||||
current_tab := self.tab_widget.CurrentIndex()
|
||||
|
||||
// Add refresh action to context menu
|
||||
refresh_action := qt.NewQAction5("&Refresh", self.QObject)
|
||||
refresh_action.OnTriggered(self.refresh_data)
|
||||
context_menu.AddAction(refresh_action)
|
||||
|
||||
context_menu.AddSeparator()
|
||||
|
||||
if current_tab == ContainersTab {
|
||||
terminal_action := qt.NewQAction5("&Terminal", self.QObject)
|
||||
terminal_action.OnTriggered(func() { self.handle_action("Terminal") })
|
||||
context_menu.AddAction(terminal_action)
|
||||
|
||||
logs_action := qt.NewQAction5("&Logs", self.QObject)
|
||||
logs_action.OnTriggered(func() { self.open_logs() })
|
||||
context_menu.AddAction(logs_action)
|
||||
|
||||
context_menu.AddSeparator()
|
||||
|
||||
start_action := qt.NewQAction5("&Start", self.QObject)
|
||||
start_action.OnTriggered(func() { self.handle_action("Start") })
|
||||
context_menu.AddAction(start_action)
|
||||
|
||||
stop_action := qt.NewQAction5("S&top", self.QObject)
|
||||
stop_action.OnTriggered(func() { self.handle_action("Stop") })
|
||||
context_menu.AddAction(stop_action)
|
||||
|
||||
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
|
||||
remove_action.OnTriggered(func() { self.handle_action("Remove") })
|
||||
context_menu.AddAction(remove_action)
|
||||
|
||||
context_menu.ExecWithPos(self.containers_tree.Viewport().MapToGlobalWithQPoint(position))
|
||||
|
||||
} else if current_tab == ImagesTab {
|
||||
pull_action := qt.NewQAction5("&Pull", self.QObject)
|
||||
pull_action.OnTriggered(self.pull_image)
|
||||
context_menu.AddAction(pull_action)
|
||||
|
||||
context_menu.AddSeparator()
|
||||
|
||||
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
|
||||
remove_action.OnTriggered(self.remove_image)
|
||||
context_menu.AddAction(remove_action)
|
||||
|
||||
context_menu.ExecWithPos(self.images_tree.Viewport().MapToGlobalWithQPoint(position))
|
||||
|
||||
} else if current_tab == NetworksTab {
|
||||
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
|
||||
remove_action.OnTriggered(func() { self.handle_action("Remove") })
|
||||
context_menu.AddAction(remove_action)
|
||||
|
||||
context_menu.ExecWithPos(self.networks_tree.Viewport().MapToGlobalWithQPoint(position))
|
||||
|
||||
} else if current_tab == VolumesTab {
|
||||
remove_action := qt.NewQAction5("Remo&ve", self.QObject)
|
||||
remove_action.OnTriggered(func() { self.handle_action("Remove") })
|
||||
context_menu.AddAction(remove_action)
|
||||
|
||||
context_menu.ExecWithPos(self.volumes_tree.Viewport().MapToGlobalWithQPoint(position))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (self *DockerGUI) setup_auto_refresh() {
|
||||
self.refresh_timer = time.NewTicker(AutoRefreshInterval)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, ok := <-self.refresh_timer.C
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
mainthread.Wait(func() {
|
||||
if !self.auto_refresh_checkbox.IsChecked() {
|
||||
return
|
||||
}
|
||||
|
||||
self.refresh_data()
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) handle_action(action string) {
|
||||
current_tab := self.tab_widget.CurrentIndex()
|
||||
|
||||
if current_tab == ContainersTab {
|
||||
selected_items := self.containers_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
container_id := selected_items[0].Text(0)
|
||||
if action == "Terminal" {
|
||||
self.open_terminal()
|
||||
} else if action == "Start" {
|
||||
dockerCommand("start", container_id).Run()
|
||||
} else if action == "Stop" {
|
||||
dockerCommand("stop", container_id).Run()
|
||||
} else if action == "Remove" {
|
||||
dockerCommand("rm", "-f", container_id).Run()
|
||||
}
|
||||
|
||||
} else if current_tab == NetworksTab {
|
||||
selected_items := self.networks_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
network_id := selected_items[0].Text(0)
|
||||
if action == "Remove" {
|
||||
dockerCommand("network", "rm", network_id).Run()
|
||||
}
|
||||
|
||||
} else if current_tab == VolumesTab {
|
||||
selected_items := self.volumes_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
volume_name := selected_items[0].Text(0)
|
||||
if action == "Remove" {
|
||||
dockerCommand("volume", "rm", volume_name).Run()
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_data()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) refresh_data() {
|
||||
self.refresh_containers()
|
||||
self.refresh_images()
|
||||
self.refresh_networks()
|
||||
self.refresh_volumes()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) refresh_containers() {
|
||||
output, err := dockerCommand("ps", "-a", "--format", "{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}").Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
scroll_position := self.containers_tree.VerticalScrollBar().Value()
|
||||
selected_items := self.get_selected_items(self.containers_tree)
|
||||
self.containers_tree.Clear()
|
||||
|
||||
containers := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, container := range containers {
|
||||
parts := strings.Split(container, "\t")
|
||||
if len(parts) == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
id := parts[0]
|
||||
name := parts[1]
|
||||
image := parts[2]
|
||||
status := parts[3]
|
||||
ports := ""
|
||||
if len(parts) > 4 {
|
||||
ports = parts[4]
|
||||
}
|
||||
|
||||
item := qt.NewQTreeWidgetItem2([]string{id, name, image, "", ports}) // Empty string for status column
|
||||
status_widget := NewStatusDelegate(status)
|
||||
self.containers_tree.AddTopLevelItem(item)
|
||||
self.containers_tree.SetItemWidget(item, 3, status_widget)
|
||||
}
|
||||
|
||||
self.filter_tree(self.containers_tree, self.containers_searchbar.Text())
|
||||
self.restore_selection(self.containers_tree, selected_items)
|
||||
|
||||
self.containers_tree.VerticalScrollBar().SetValue(scroll_position)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) refresh_images() {
|
||||
output, err := dockerCommand("images", "--format", "{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.Size}}").Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
scroll_position := self.images_tree.VerticalScrollBar().Value()
|
||||
selected_items := self.get_selected_items(self.images_tree)
|
||||
self.images_tree.Clear()
|
||||
|
||||
images := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, image := range images {
|
||||
parts := strings.Split(image, "\t")
|
||||
if len(parts) == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
id := parts[0]
|
||||
repository := parts[1]
|
||||
tag := parts[2]
|
||||
size := parts[3]
|
||||
|
||||
item := qt.NewQTreeWidgetItem2([]string{id, repository, tag, size})
|
||||
self.images_tree.AddTopLevelItem(item)
|
||||
}
|
||||
|
||||
self.filter_tree(self.images_tree, self.images_searchbar.Text())
|
||||
self.restore_selection(self.images_tree, selected_items)
|
||||
|
||||
self.images_tree.VerticalScrollBar().SetValue(scroll_position)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) refresh_networks() {
|
||||
output, err := dockerCommand("network", "ls", "--format", "{{.ID}}\\t{{.Name}}\\t{{.Driver}}").Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
scroll_position := self.networks_tree.VerticalScrollBar().Value()
|
||||
selected_items := self.get_selected_items(self.networks_tree)
|
||||
self.networks_tree.Clear()
|
||||
|
||||
networks := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, network := range networks {
|
||||
parts := strings.Split(network, "\t")
|
||||
if len(parts) == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
id := parts[0]
|
||||
name := parts[1]
|
||||
driver := parts[2]
|
||||
|
||||
item := qt.NewQTreeWidgetItem2([]string{id, name, driver})
|
||||
self.networks_tree.AddTopLevelItem(item)
|
||||
}
|
||||
self.filter_tree(self.networks_tree, self.networks_searchbar.Text())
|
||||
self.restore_selection(self.networks_tree, selected_items)
|
||||
|
||||
self.networks_tree.VerticalScrollBar().SetValue(scroll_position)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) refresh_volumes() {
|
||||
output, err := dockerCommand("volume", "ls", "--format", "{{.Name}}\\t{{.Driver}}\\t{{.Mountpoint}}").Output()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
scroll_position := self.volumes_tree.VerticalScrollBar().Value()
|
||||
selected_items := self.get_selected_items(self.volumes_tree)
|
||||
self.volumes_tree.Clear()
|
||||
|
||||
volumes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, volume := range volumes {
|
||||
parts := strings.Split(volume, "\t")
|
||||
if len(parts) == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := parts[0]
|
||||
driver := parts[1]
|
||||
mountpoint := parts[2]
|
||||
|
||||
item := qt.NewQTreeWidgetItem2([]string{name, driver, mountpoint})
|
||||
self.volumes_tree.AddTopLevelItem(item)
|
||||
}
|
||||
self.filter_tree(self.volumes_tree, self.volumes_searchbar.Text())
|
||||
self.restore_selection(self.volumes_tree, selected_items)
|
||||
|
||||
self.volumes_tree.VerticalScrollBar().SetValue(scroll_position)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) get_selected_items(tree *qt.QTreeWidget) []string {
|
||||
selItems := tree.SelectedItems()
|
||||
|
||||
ret := make([]string, 0, len(selItems))
|
||||
for _, itm := range selItems {
|
||||
ret = append(ret, itm.Text(0))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (self *DockerGUI) restore_selection(tree *qt.QTreeWidget, selected_items []string) {
|
||||
|
||||
rmatch := map[string]struct{}{}
|
||||
for _, itm := range selected_items {
|
||||
rmatch[itm] = struct{}{}
|
||||
}
|
||||
|
||||
for i := 0; i < tree.TopLevelItemCount(); i++ {
|
||||
item := tree.TopLevelItem(i)
|
||||
|
||||
if _, ok := rmatch[item.Text(0)]; ok {
|
||||
item.SetSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DockerGUI) start_container() {
|
||||
selected_items := self.containers_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to start.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range selected_items {
|
||||
container_id := item.Text(0)
|
||||
|
||||
err := dockerCommand("start", container_id).Wait()
|
||||
if err != nil {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to start container "+container_id+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_containers()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) stop_container() {
|
||||
selected_items := self.containers_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to stop.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range selected_items {
|
||||
container_id := item.Text(0)
|
||||
|
||||
err := dockerCommand("stop", container_id).Run()
|
||||
if err != nil {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to stop container "+container_id+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_containers()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) remove_container() {
|
||||
selected_items := self.containers_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to remove.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range selected_items {
|
||||
container_id := item.Text(0)
|
||||
reply := qt.QMessageBox_Question5(self.QWidget,
|
||||
"Confirm Removal",
|
||||
"Are you sure you want to remove container "+container_id+"?",
|
||||
qt.QMessageBox__Yes|qt.QMessageBox__No,
|
||||
qt.QMessageBox__No,
|
||||
)
|
||||
|
||||
if reply != qt.QMessageBox__Yes {
|
||||
continue
|
||||
}
|
||||
|
||||
err := dockerCommand("rm", "-f", container_id).Run()
|
||||
if err != nil {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove container "+container_id+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_containers()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) pull_image() {
|
||||
image_name := qt.QInputDialog_GetText(self.QWidget, "Pull Image", "Enter image name (e.g., ubuntu:latest):")
|
||||
if image_name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := dockerCommand("pull", image_name).Run()
|
||||
if err == nil {
|
||||
qt.QMessageBox_Information(self.QWidget, "Success", "Image \""+image_name+"\" pulled successfully.")
|
||||
} else {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to pull image: "+err.Error())
|
||||
}
|
||||
|
||||
self.refresh_images()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) remove_image() {
|
||||
selected_items := self.images_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select an image to remove.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range selected_items {
|
||||
image_id := item.Text(0)
|
||||
reply := qt.QMessageBox_Question5(self.QWidget, "Confirm Removal",
|
||||
"Are you sure you want to remove image "+image_id+"?",
|
||||
qt.QMessageBox__Yes|qt.QMessageBox__No,
|
||||
qt.QMessageBox__No,
|
||||
)
|
||||
if reply != qt.QMessageBox__Yes {
|
||||
continue
|
||||
}
|
||||
|
||||
err := dockerCommand("rmi", image_id).Run()
|
||||
if err == nil {
|
||||
qt.QMessageBox_Information(self.QWidget, "Success", "Image \""+image_id+"\" removed successfully.")
|
||||
} else {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove image "+image_id+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_images()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) create_network() {
|
||||
|
||||
network_name := qt.QInputDialog_GetText(self.QWidget, "Create Network", "Enter network name:")
|
||||
if network_name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := dockerCommand("network", "create", network_name).Run()
|
||||
if err == nil {
|
||||
qt.QMessageBox_Information(self.QWidget, "Success", "Network \""+network_name+"\" created successfully.")
|
||||
} else {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to create network: "+err.Error())
|
||||
}
|
||||
|
||||
self.refresh_networks()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) remove_network() {
|
||||
selected_items := self.networks_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a network to remove.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range selected_items {
|
||||
network_name := item.Text(1) // Assuming the network name is in the second column
|
||||
reply := qt.QMessageBox_Question5(self.QWidget, "Confirm Removal",
|
||||
"Are you sure you want to remove network "+network_name+"?",
|
||||
qt.QMessageBox__Yes|qt.QMessageBox__No,
|
||||
qt.QMessageBox__No,
|
||||
)
|
||||
if reply != qt.QMessageBox__Yes {
|
||||
continue
|
||||
}
|
||||
|
||||
err := dockerCommand("network", "rm", network_name).Run()
|
||||
if err == nil {
|
||||
qt.QMessageBox_Information(self.QWidget, "Success", "Network \""+network_name+"\" removed successfully.")
|
||||
} else {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove network "+network_name+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_networks()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) create_volume() {
|
||||
volume_name := qt.QInputDialog_GetText(self.QWidget, "Create Volume", "Enter volume name:")
|
||||
if volume_name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := dockerCommand("volume", "create", volume_name).Run()
|
||||
if err == nil {
|
||||
qt.QMessageBox_Information(self.QWidget, "Success", "Volume \""+volume_name+"\" created successfully.")
|
||||
} else {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to create volume "+volume_name+": "+err.Error())
|
||||
}
|
||||
|
||||
self.refresh_volumes()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) remove_volume() {
|
||||
selected_items := self.volumes_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a volume to remove.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range selected_items {
|
||||
volume_name := item.Text(0) // Assuming the volume name is in the first column
|
||||
reply := qt.QMessageBox_Question5(self.QWidget, "Confirm Removal",
|
||||
"Are you sure you want to remove volume "+volume_name+"?",
|
||||
qt.QMessageBox__Yes|qt.QMessageBox__No,
|
||||
qt.QMessageBox__No,
|
||||
)
|
||||
if reply != qt.QMessageBox__Yes {
|
||||
continue
|
||||
}
|
||||
|
||||
err := dockerCommand("volume", "rm", volume_name).Run()
|
||||
if err == nil {
|
||||
qt.QMessageBox_Information(self.QWidget, "Success", "Volume \""+volume_name+"\" removed successfully.")
|
||||
} else {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", "Failed to remove volume "+volume_name+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_volumes()
|
||||
}
|
||||
|
||||
func (self *DockerGUI) open_terminal() {
|
||||
selected_items := self.containers_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to open terminal.")
|
||||
return
|
||||
}
|
||||
|
||||
container_id := selected_items[0].Text(0)
|
||||
|
||||
openTerminal(container_id)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) show_terminal_error(error_message string) {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", error_message)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) open_logs() {
|
||||
selected_items := self.containers_tree.SelectedItems()
|
||||
if len(selected_items) == 0 {
|
||||
qt.QMessageBox_Warning(self.QWidget, "No Selection", "Please select a container to open logs.")
|
||||
return
|
||||
}
|
||||
|
||||
container_id := selected_items[0].Text(0)
|
||||
openLogs(container_id)
|
||||
}
|
||||
|
||||
func (self *DockerGUI) show_logs_error(error_message string) {
|
||||
qt.QMessageBox_Critical(self.QWidget, "Error", error_message)
|
||||
}
|
||||
|
||||
func main() {
|
||||
qt.NewQApplication(os.Args)
|
||||
|
||||
for _, arg := range os.Args {
|
||||
if arg == `--sudo` {
|
||||
dockerSudo = true
|
||||
}
|
||||
}
|
||||
|
||||
window := NewDockerGUI()
|
||||
window.Show()
|
||||
os.Exit(qt.QApplication_Exec())
|
||||
}
|
664
main.py
664
main.py
@ -1,664 +0,0 @@
|
||||
import sys
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTabWidget, QTreeWidget, QTreeWidgetItem,
|
||||
QVBoxLayout, QHBoxLayout, QWidget, QToolBar, QAction, QMenu,
|
||||
QHeaderView, QLabel, QLineEdit, QCheckBox, QMessageBox, QInputDialog)
|
||||
from PyQt5.QtGui import QIcon, QColor
|
||||
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
class StatusDelegate(QWidget):
|
||||
def __init__(self, status, parent=None):
|
||||
super().__init__(parent)
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(4, 4, 4, 4)
|
||||
layout.setSpacing(8)
|
||||
|
||||
self.status_circle = QWidget()
|
||||
self.status_circle.setFixedSize(12, 12)
|
||||
color = QColor('green') if 'Up' in status else QColor('red')
|
||||
self.status_circle.setStyleSheet(f"background-color: {color.name()}; border-radius: 6px;")
|
||||
|
||||
self.status_label = QLabel(status)
|
||||
|
||||
layout.addWidget(self.status_circle)
|
||||
layout.addWidget(self.status_label)
|
||||
layout.addStretch()
|
||||
|
||||
class TerminalOpener(QThread):
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, container_id):
|
||||
super().__init__()
|
||||
self.container_id = container_id
|
||||
|
||||
def run(self):
|
||||
docker_command = f"docker exec -it {self.container_id} sh -c '[ -x /bin/bash ] && exec /bin/bash || exec /bin/sh'"
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Darwin": # macOS
|
||||
subprocess.Popen(['open', '-a', 'Terminal', '--', 'sh', '-c', f"{docker_command}"])
|
||||
elif system == "Linux":
|
||||
subprocess.Popen(['x-terminal-emulator', '-e', f'sh -c "{docker_command}"'])
|
||||
elif system == "Windows":
|
||||
subprocess.Popen(['start', 'cmd', '/k', docker_command], shell=True)
|
||||
else:
|
||||
self.error.emit(f"Opening a terminal is not supported on {system}")
|
||||
except Exception as e:
|
||||
self.error.emit(f"Failed to open terminal: {str(e)}")
|
||||
|
||||
class LogsOpener(QThread):
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, container_id):
|
||||
super().__init__()
|
||||
self.container_id = container_id
|
||||
|
||||
def run(self):
|
||||
docker_command = f"docker logs -f {self.container_id}"
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Darwin": # macOS
|
||||
subprocess.Popen(['open', '-a', 'Terminal', '--', 'sh', '-c', f"{docker_command}"])
|
||||
elif system == "Linux":
|
||||
subprocess.Popen(['x-terminal-emulator', '-e', f'sh -c "{docker_command}"'])
|
||||
elif system == "Windows":
|
||||
subprocess.Popen(['start', 'cmd', '/k', docker_command], shell=True)
|
||||
else:
|
||||
self.error.emit(f"Opening a terminal is not supported on {system}")
|
||||
except Exception as e:
|
||||
self.error.emit(f"Failed to open terminal: {str(e)}")
|
||||
|
||||
class DockerGUI(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Qocker - Docker Graphical User Interface")
|
||||
self.setGeometry(100, 100, 1000, 600)
|
||||
|
||||
# Create central widget and layout
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
main_layout = QVBoxLayout(central_widget)
|
||||
|
||||
# Create toolbar
|
||||
self.create_toolbar()
|
||||
|
||||
# Create tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Create tabs
|
||||
self.containers_tab = QWidget()
|
||||
self.images_tab = QWidget()
|
||||
self.networks_tab = QWidget()
|
||||
self.volumes_tab = QWidget()
|
||||
|
||||
self.tab_widget.addTab(self.containers_tab, "Containers")
|
||||
self.tab_widget.addTab(self.images_tab, "Images")
|
||||
self.tab_widget.addTab(self.networks_tab, "Networks")
|
||||
self.tab_widget.addTab(self.volumes_tab, "Volumes")
|
||||
|
||||
# Connect tab change to toolbar update
|
||||
self.tab_widget.currentChanged.connect(self.update_toolbar_buttons)
|
||||
|
||||
# Create tree widgets for each tab
|
||||
self.containers_tree = self.create_tree_widget(["ID", "Name", "Image", "Status", "Ports"])
|
||||
self.images_tree = self.create_tree_widget(["ID", "Repository", "Tag", "Size"])
|
||||
self.networks_tree = self.create_tree_widget(["ID", "Name", "Driver"])
|
||||
self.volumes_tree = self.create_tree_widget(["Name", "Driver", "Mountpoint"])
|
||||
|
||||
self.containers_tree.itemDoubleClicked.connect(self.open_terminal)
|
||||
|
||||
# Add tree widgets to tabs
|
||||
self.setup_tab(self.containers_tab, self.containers_tree, "Search containers...")
|
||||
self.setup_tab(self.images_tab, self.images_tree, "Search images...")
|
||||
self.setup_tab(self.networks_tab, self.networks_tree, "Search networks...")
|
||||
self.setup_tab(self.volumes_tab, self.volumes_tree, "Search volumes...")
|
||||
|
||||
# Create menu bar
|
||||
self.create_menu_bar()
|
||||
|
||||
# Setup auto-refresh
|
||||
self.setup_auto_refresh()
|
||||
|
||||
# Populate data
|
||||
self.refresh_data()
|
||||
|
||||
# Update toolbar buttons for initial state
|
||||
self.update_toolbar_buttons(0)
|
||||
|
||||
def create_tree_widget(self, headers):
|
||||
tree = QTreeWidget()
|
||||
tree.setHeaderLabels(headers)
|
||||
tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
tree.customContextMenuRequested.connect(self.show_context_menu)
|
||||
tree.header().setSectionResizeMode(QHeaderView.Interactive)
|
||||
tree.header().setSortIndicator(0, Qt.DescendingOrder)
|
||||
tree.header().setSortIndicatorShown(True)
|
||||
tree.header().sortIndicatorChanged.connect(lambda col, order: self.sort_tree_widget(tree, col, order))
|
||||
return tree
|
||||
|
||||
def sort_tree_widget(self, tree, column, order):
|
||||
tree.sortItems(column, order)
|
||||
|
||||
def setup_tab(self, tab, tree, search_placeholder):
|
||||
layout = QVBoxLayout(tab)
|
||||
# Add search bar
|
||||
search_bar = QLineEdit()
|
||||
search_bar.setPlaceholderText(search_placeholder)
|
||||
search_bar.textChanged.connect(lambda text: self.filter_tree(tree, text))
|
||||
layout.addWidget(search_bar)
|
||||
|
||||
layout.addWidget(tree)
|
||||
|
||||
def filter_tree(self, tree, text):
|
||||
for i in range(tree.topLevelItemCount()):
|
||||
item = tree.topLevelItem(i)
|
||||
match = any(text.lower() in item.text(j).lower() for j in range(item.columnCount()))
|
||||
item.setHidden(not match)
|
||||
|
||||
def create_toolbar(self):
|
||||
self.toolbar = QToolBar()
|
||||
self.toolbar.setMovable(False) # Make toolbar fixed
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
|
||||
|
||||
# Common actions
|
||||
self.refresh_action = QAction(QIcon.fromTheme("view-refresh"), "Refresh", self)
|
||||
self.refresh_action.triggered.connect(self.refresh_data)
|
||||
self.toolbar.addAction(self.refresh_action)
|
||||
|
||||
# Add auto-refresh checkbox
|
||||
self.auto_refresh_checkbox = QCheckBox("Auto-refresh")
|
||||
self.auto_refresh_checkbox.setChecked(True)
|
||||
self.auto_refresh_checkbox.stateChanged.connect(self.toggle_auto_refresh)
|
||||
self.toolbar.addWidget(self.auto_refresh_checkbox)
|
||||
|
||||
# Add separator
|
||||
self.toolbar.addSeparator()
|
||||
|
||||
# Container-specific actions
|
||||
self.start_action = QAction(QIcon.fromTheme("media-playback-start"), "Start", self)
|
||||
self.start_action.triggered.connect(self.start_container)
|
||||
self.toolbar.addAction(self.start_action)
|
||||
|
||||
self.stop_action = QAction(QIcon.fromTheme("media-playback-stop"), "Stop", self)
|
||||
self.stop_action.triggered.connect(self.stop_container)
|
||||
self.toolbar.addAction(self.stop_action)
|
||||
|
||||
self.remove_action = QAction(QIcon.fromTheme("edit-delete"), "Remove", self)
|
||||
self.remove_action.triggered.connect(self.remove_container)
|
||||
self.toolbar.addAction(self.remove_action)
|
||||
|
||||
# Image-specific actions
|
||||
self.pull_image_action = QAction(QIcon.fromTheme("download"), "Pull Image", self)
|
||||
self.pull_image_action.triggered.connect(self.pull_image)
|
||||
self.toolbar.addAction(self.pull_image_action)
|
||||
|
||||
self.remove_image_action = QAction(QIcon.fromTheme("edit-delete"), "Remove Image", self)
|
||||
self.remove_image_action.triggered.connect(self.remove_image)
|
||||
self.toolbar.addAction(self.remove_image_action)
|
||||
|
||||
# Network-specific actions
|
||||
self.create_network_action = QAction(QIcon.fromTheme("list-add"), "Create Network", self)
|
||||
self.create_network_action.triggered.connect(self.create_network)
|
||||
self.toolbar.addAction(self.create_network_action)
|
||||
|
||||
self.remove_network_action = QAction(QIcon.fromTheme("edit-delete"), "Remove Network", self)
|
||||
self.remove_network_action.triggered.connect(self.remove_network)
|
||||
self.toolbar.addAction(self.remove_network_action)
|
||||
|
||||
# Volume-specific actions
|
||||
self.create_volume_action = QAction(QIcon.fromTheme("list-add"), "Create Volume", self)
|
||||
self.create_volume_action.triggered.connect(self.create_volume)
|
||||
self.toolbar.addAction(self.create_volume_action)
|
||||
|
||||
self.remove_volume_action = QAction(QIcon.fromTheme("edit-delete"), "Remove Volume", self)
|
||||
self.remove_volume_action.triggered.connect(self.remove_volume)
|
||||
self.toolbar.addAction(self.remove_volume_action)
|
||||
|
||||
# Add terminal action
|
||||
self.terminal_action = QAction(QIcon.fromTheme("utilities-terminal"), "Open Terminal", self)
|
||||
self.terminal_action.triggered.connect(self.open_terminal)
|
||||
self.toolbar.addAction(self.terminal_action)
|
||||
|
||||
# Add logs action
|
||||
self.logs_action = QAction(QIcon.fromTheme("document-open"), "Open Logs", self)
|
||||
self.logs_action.triggered.connect(self.open_logs)
|
||||
self.toolbar.addAction(self.logs_action)
|
||||
|
||||
def update_toolbar_buttons(self, index):
|
||||
# Hide all specific actions
|
||||
self.start_action.setVisible(False)
|
||||
self.stop_action.setVisible(False)
|
||||
self.remove_action.setVisible(False)
|
||||
self.create_network_action.setVisible(False)
|
||||
self.remove_network_action.setVisible(False)
|
||||
self.create_volume_action.setVisible(False)
|
||||
self.remove_volume_action.setVisible(False)
|
||||
self.terminal_action.setVisible(False)
|
||||
self.pull_image_action.setVisible(False)
|
||||
self.remove_image_action.setVisible(False)
|
||||
self.logs_action.setVisible(False)
|
||||
|
||||
# Show actions based on the current tab
|
||||
if index == 0: # Containers tab
|
||||
self.start_action.setVisible(True)
|
||||
self.stop_action.setVisible(True)
|
||||
self.remove_action.setVisible(True)
|
||||
self.terminal_action.setVisible(True)
|
||||
self.logs_action.setVisible(True)
|
||||
elif index == 1: # Images tab
|
||||
self.pull_image_action.setVisible(True)
|
||||
self.remove_image_action.setVisible(True)
|
||||
elif index == 2: # Networks tab
|
||||
self.create_network_action.setVisible(True)
|
||||
self.remove_network_action.setVisible(True)
|
||||
elif index == 3: # Volumes tab
|
||||
self.create_volume_action.setVisible(True)
|
||||
self.remove_volume_action.setVisible(True)
|
||||
|
||||
# override QMainWindow.createPopupMenu
|
||||
def createPopupMenu(self):
|
||||
filtered_menu = super(DockerGUI, self).createPopupMenu()
|
||||
filtered_menu.removeAction(self.toolbar.toggleViewAction())
|
||||
|
||||
return filtered_menu
|
||||
|
||||
def update_visible_tabs(self, index):
|
||||
for i in range(self.tab_widget.count()):
|
||||
if i == index:
|
||||
self.tab_widget.tabBar().setTabVisible(i, True)
|
||||
else:
|
||||
self.tab_widget.tabBar().setTabVisible(i, False)
|
||||
|
||||
def create_menu_bar(self):
|
||||
menubar = self.menuBar()
|
||||
|
||||
file_menu = menubar.addMenu("File")
|
||||
exit_action = QAction("Exit", self)
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
docker_menu = menubar.addMenu("Docker")
|
||||
refresh_action = QAction("Refresh", self)
|
||||
refresh_action.triggered.connect(self.refresh_data)
|
||||
docker_menu.addAction(refresh_action)
|
||||
|
||||
def show_context_menu(self, position):
|
||||
context_menu = QMenu()
|
||||
current_tab = self.tab_widget.currentWidget()
|
||||
|
||||
# Add refresh action to context menu
|
||||
refresh_action = QAction("Refresh", self)
|
||||
refresh_action.triggered.connect(self.refresh_data)
|
||||
context_menu.addAction(refresh_action)
|
||||
|
||||
context_menu.addSeparator()
|
||||
|
||||
if current_tab == self.containers_tab:
|
||||
terminal_action = QAction("Terminal", self)
|
||||
terminal_action.triggered.connect(lambda: self.handle_action("Terminal"))
|
||||
logs_action = QAction("Logs", self)
|
||||
logs_action.triggered.connect(lambda: self.open_logs())
|
||||
start_action = QAction("Start", self)
|
||||
start_action.triggered.connect(lambda: self.handle_action("Start"))
|
||||
stop_action = QAction("Stop", self)
|
||||
stop_action.triggered.connect(lambda: self.handle_action("Stop"))
|
||||
remove_action = QAction("Remove", self)
|
||||
remove_action.triggered.connect(lambda: self.handle_action("Remove"))
|
||||
context_menu.addAction(terminal_action)
|
||||
context_menu.addAction(logs_action)
|
||||
context_menu.addSeparator()
|
||||
context_menu.addAction(start_action)
|
||||
context_menu.addAction(stop_action)
|
||||
context_menu.addAction(remove_action)
|
||||
elif current_tab == self.images_tab:
|
||||
pull_action = QAction("Pull", self)
|
||||
pull_action.triggered.connect(self.pull_image)
|
||||
remove_action = QAction("Remove", self)
|
||||
remove_action.triggered.connect(self.remove_image)
|
||||
context_menu.addAction(pull_action)
|
||||
context_menu.addSeparator()
|
||||
context_menu.addAction(remove_action)
|
||||
elif current_tab == self.networks_tab:
|
||||
remove_action = QAction("Remove", self)
|
||||
remove_action.triggered.connect(lambda: self.handle_action("Remove"))
|
||||
context_menu.addAction(remove_action)
|
||||
elif current_tab == self.volumes_tab:
|
||||
remove_action = QAction("Remove", self)
|
||||
remove_action.triggered.connect(lambda: self.handle_action("Remove"))
|
||||
context_menu.addAction(remove_action)
|
||||
|
||||
context_menu.exec_(current_tab.mapToGlobal(position))
|
||||
|
||||
def setup_auto_refresh(self):
|
||||
self.refresh_timer = QTimer(self)
|
||||
self.refresh_timer.timeout.connect(self.refresh_data)
|
||||
if self.auto_refresh_checkbox.isChecked():
|
||||
self.refresh_timer.start(1000) # 1000 ms = 1 second
|
||||
|
||||
def toggle_auto_refresh(self, state):
|
||||
if state == Qt.Checked:
|
||||
self.refresh_timer.start(1000)
|
||||
else:
|
||||
self.refresh_timer.stop()
|
||||
|
||||
def handle_action(self, action):
|
||||
current_tab = self.tab_widget.currentWidget()
|
||||
selected_items = current_tab.findChild(QTreeWidget).selectedItems()
|
||||
|
||||
if not selected_items:
|
||||
return
|
||||
|
||||
if current_tab == self.containers_tab:
|
||||
container_id = selected_items[0].text(0)
|
||||
if action == "Terminal":
|
||||
self.open_terminal()
|
||||
elif action == "Start":
|
||||
subprocess.run(["docker", "start", container_id])
|
||||
elif action == "Stop":
|
||||
subprocess.run(["docker", "stop", container_id])
|
||||
elif action == "Remove":
|
||||
subprocess.run(["docker", "rm", "-f", container_id])
|
||||
elif current_tab == self.networks_tab:
|
||||
network_id = selected_items[0].text(0)
|
||||
if action == "Remove":
|
||||
subprocess.run(["docker", "network", "rm", network_id])
|
||||
elif current_tab == self.volumes_tab:
|
||||
volume_name = selected_items[0].text(0)
|
||||
if action == "Remove":
|
||||
subprocess.run(["docker", "volume", "rm", volume_name])
|
||||
|
||||
self.refresh_data()
|
||||
|
||||
def refresh_data(self):
|
||||
self.refresh_containers()
|
||||
self.refresh_images()
|
||||
self.refresh_networks()
|
||||
self.refresh_volumes()
|
||||
|
||||
def refresh_containers(self):
|
||||
scroll_position = self.containers_tree.verticalScrollBar().value()
|
||||
selected_items = self.get_selected_items(self.containers_tree)
|
||||
self.containers_tree.clear()
|
||||
try:
|
||||
output = subprocess.check_output(["docker", "ps", "-a", "--format", "{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}"], stderr=subprocess.STDOUT)
|
||||
if output.strip():
|
||||
containers = output.decode().strip().split("\n")
|
||||
for container in containers:
|
||||
parts = container.split("\t")
|
||||
id, name, image, status = parts[:4]
|
||||
ports = parts[4] if len(parts) > 4 else ""
|
||||
item = QTreeWidgetItem([id, name, image, "", ports]) # Empty string for status column
|
||||
status_widget = StatusDelegate(status)
|
||||
self.containers_tree.addTopLevelItem(item)
|
||||
self.containers_tree.setItemWidget(item, 3, status_widget)
|
||||
self.filter_tree(self.containers_tree, self.containers_tab.findChild(QLineEdit).text())
|
||||
self.restore_selection(self.containers_tree, selected_items)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error refreshing containers: {e.output.decode()}")
|
||||
except ValueError:
|
||||
print(f"Error parsing container list {repr(containers)}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error refreshing containers: {str(e)}")
|
||||
QTimer.singleShot(0, lambda: self.containers_tree.verticalScrollBar().setValue(scroll_position))
|
||||
|
||||
def refresh_images(self):
|
||||
scroll_position = self.images_tree.verticalScrollBar().value()
|
||||
selected_items = self.get_selected_items(self.images_tree)
|
||||
self.images_tree.clear()
|
||||
try:
|
||||
output = subprocess.check_output(["docker", "images", "--format", "{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.Size}}"], stderr=subprocess.STDOUT)
|
||||
if output.strip():
|
||||
images = output.decode().strip().split("\n")
|
||||
for image in images:
|
||||
id, repository, tag, size = image.split("\t")
|
||||
item = QTreeWidgetItem([id, repository, tag, size])
|
||||
self.images_tree.addTopLevelItem(item)
|
||||
|
||||
self.sort_tree_widget(self.images_tree, 0, Qt.DescendingOrder)
|
||||
self.filter_tree(self.images_tree, self.images_tab.findChild(QLineEdit).text())
|
||||
self.restore_selection(self.images_tree, selected_items)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error refreshing images: {e.output.decode()}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error refreshing images: {str(e)}")
|
||||
QTimer.singleShot(0, lambda: self.images_tree.verticalScrollBar().setValue(scroll_position))
|
||||
|
||||
def refresh_networks(self):
|
||||
scroll_position = self.networks_tree.verticalScrollBar().value()
|
||||
selected_items = self.get_selected_items(self.networks_tree)
|
||||
self.networks_tree.clear()
|
||||
try:
|
||||
output = subprocess.check_output(["docker", "network", "ls", "--format", "{{.ID}}\\t{{.Name}}\\t{{.Driver}}"], stderr=subprocess.STDOUT)
|
||||
if output.strip():
|
||||
networks = output.decode().strip().split("\n")
|
||||
for network in networks:
|
||||
id, name, driver = network.split("\t")
|
||||
item = QTreeWidgetItem([id, name, driver])
|
||||
self.networks_tree.addTopLevelItem(item)
|
||||
self.filter_tree(self.networks_tree, self.networks_tab.findChild(QLineEdit).text())
|
||||
self.restore_selection(self.networks_tree, selected_items)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error refreshing networks: {e.output.decode()}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error refreshing networks: {str(e)}")
|
||||
QTimer.singleShot(0, lambda: self.networks_tree.verticalScrollBar().setValue(scroll_position))
|
||||
|
||||
def refresh_volumes(self):
|
||||
scroll_position = self.volumes_tree.verticalScrollBar().value()
|
||||
selected_items = self.get_selected_items(self.volumes_tree)
|
||||
self.volumes_tree.clear()
|
||||
try:
|
||||
output = subprocess.check_output(["docker", "volume", "ls", "--format", "{{.Name}}\\t{{.Driver}}\\t{{.Mountpoint}}"], stderr=subprocess.STDOUT)
|
||||
if output.strip():
|
||||
volumes = output.decode().strip().split("\n")
|
||||
for volume in volumes:
|
||||
name, driver, mountpoint = volume.split("\t")
|
||||
item = QTreeWidgetItem([name, driver, mountpoint])
|
||||
self.volumes_tree.addTopLevelItem(item)
|
||||
self.filter_tree(self.volumes_tree, self.volumes_tab.findChild(QLineEdit).text())
|
||||
self.restore_selection(self.volumes_tree, selected_items)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error refreshing volumes: {e.output.decode()}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error refreshing volumes: {str(e)}")
|
||||
QTimer.singleShot(0, lambda: self.volumes_tree.verticalScrollBar().setValue(scroll_position))
|
||||
|
||||
def get_selected_items(self, tree):
|
||||
return [item.text(0) for item in tree.selectedItems()]
|
||||
|
||||
def restore_selection(self, tree, selected_items):
|
||||
for i in range(tree.topLevelItemCount()):
|
||||
item = tree.topLevelItem(i)
|
||||
if item.text(0) in selected_items:
|
||||
item.setSelected(True)
|
||||
|
||||
def start_container(self):
|
||||
selected_items = self.containers_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a container to start.")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
container_id = item.text(0)
|
||||
try:
|
||||
subprocess.run(["docker", "start", container_id], check=True)
|
||||
print(f"Started container: {container_id}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to start container {container_id}: {e}")
|
||||
|
||||
self.refresh_containers()
|
||||
|
||||
def stop_container(self):
|
||||
selected_items = self.containers_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a container to stop.")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
container_id = item.text(0)
|
||||
try:
|
||||
subprocess.run(["docker", "stop", container_id], check=True)
|
||||
print(f"Stopped container: {container_id}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to stop container {container_id}: {e}")
|
||||
|
||||
self.refresh_containers()
|
||||
|
||||
def remove_container(self):
|
||||
selected_items = self.containers_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a container to remove.")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
container_id = item.text(0)
|
||||
reply = QMessageBox.question(self, "Confirm Removal",
|
||||
f"Are you sure you want to remove container {container_id}?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
subprocess.run(["docker", "rm", "-f", container_id], check=True)
|
||||
print(f"Removed container: {container_id}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to remove container {container_id}: {e}")
|
||||
|
||||
self.refresh_containers()
|
||||
|
||||
def pull_image(self):
|
||||
image_name, ok = QInputDialog.getText(self, "Pull Image", "Enter image name (e.g., ubuntu:latest):")
|
||||
if ok and image_name:
|
||||
try:
|
||||
subprocess.run(["docker", "pull", image_name], check=True)
|
||||
print(f"Pulled image: {image_name}")
|
||||
QMessageBox.information(self, "Success", f"Image '{image_name}' pulled successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to pull image: {e}")
|
||||
|
||||
self.refresh_images()
|
||||
|
||||
def remove_image(self):
|
||||
selected_items = self.images_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select an image to remove.")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
image_id = item.text(0)
|
||||
reply = QMessageBox.question(self, "Confirm Removal",
|
||||
f"Are you sure you want to remove image {image_id}?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
subprocess.run(["docker", "rmi", image_id], check=True)
|
||||
print(f"Removed image: {image_id}")
|
||||
QMessageBox.information(self, "Success", f"Image '{image_id}' removed successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to remove image {image_id}: {e}")
|
||||
|
||||
self.refresh_images()
|
||||
|
||||
def create_network(self):
|
||||
network_name, ok = QInputDialog.getText(self, "Create Network", "Enter network name:")
|
||||
if ok and network_name:
|
||||
try:
|
||||
subprocess.run(["docker", "network", "create", network_name], check=True)
|
||||
print(f"Created network: {network_name}")
|
||||
QMessageBox.information(self, "Success", f"Network '{network_name}' created successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to create network: {e}")
|
||||
|
||||
self.refresh_networks()
|
||||
|
||||
def remove_network(self):
|
||||
selected_items = self.networks_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a network to remove.")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
network_name = item.text(1) # Assuming the network name is in the second column
|
||||
reply = QMessageBox.question(self, "Confirm Removal",
|
||||
f"Are you sure you want to remove network {network_name}?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
subprocess.run(["docker", "network", "rm", network_name], check=True)
|
||||
print(f"Removed network: {network_name}")
|
||||
QMessageBox.information(self, "Success", f"Network '{network_name}' removed successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to remove network {network_name}: {e}")
|
||||
|
||||
self.refresh_networks()
|
||||
|
||||
def create_volume(self):
|
||||
volume_name, ok = QInputDialog.getText(self, "Create Volume", "Enter volume name:")
|
||||
if ok and volume_name:
|
||||
try:
|
||||
subprocess.run(["docker", "volume", "create", volume_name], check=True)
|
||||
print(f"Created volume: {volume_name}")
|
||||
QMessageBox.information(self, "Success", f"Volume '{volume_name}' created successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to create volume: {e}")
|
||||
|
||||
self.refresh_volumes()
|
||||
|
||||
def remove_volume(self):
|
||||
selected_items = self.volumes_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a volume to remove.")
|
||||
return
|
||||
|
||||
for item in selected_items:
|
||||
volume_name = item.text(0) # Assuming the volume name is in the first column
|
||||
reply = QMessageBox.question(self, "Confirm Removal",
|
||||
f"Are you sure you want to remove volume {volume_name}?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
subprocess.run(["docker", "volume", "rm", volume_name], check=True)
|
||||
print(f"Removed volume: {volume_name}")
|
||||
QMessageBox.information(self, "Success", f"Volume '{volume_name}' removed successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to remove volume {volume_name}: {e}")
|
||||
|
||||
self.refresh_volumes()
|
||||
|
||||
def open_terminal(self):
|
||||
selected_items = self.containers_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a container to open terminal.")
|
||||
return
|
||||
|
||||
container_id = selected_items[0].text(0)
|
||||
|
||||
self.terminal_opener = TerminalOpener(container_id)
|
||||
self.terminal_opener.error.connect(self.show_terminal_error)
|
||||
self.terminal_opener.start()
|
||||
|
||||
def show_terminal_error(self, error_message):
|
||||
QMessageBox.critical(self, "Error", error_message)
|
||||
|
||||
def open_logs(self):
|
||||
selected_items = self.containers_tree.selectedItems()
|
||||
if not selected_items:
|
||||
QMessageBox.warning(self, "No Selection", "Please select a container to open logs.")
|
||||
return
|
||||
|
||||
container_id = selected_items[0].text(0)
|
||||
|
||||
self.logs_opener = LogsOpener(container_id)
|
||||
self.logs_opener.error.connect(self.show_logs_error)
|
||||
self.logs_opener.start()
|
||||
|
||||
def show_logs_error(self, error_message):
|
||||
QMessageBox.critical(self, "Error", error_message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
window = DockerGUI()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
@ -1 +0,0 @@
|
||||
pyqt5==5.15.10
|
Loading…
x
Reference in New Issue
Block a user