Compare commits
No commits in common. "master" and "v0.1" have entirely different histories.
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
qocker-miqt
|
/.idea
|
||||||
|
/venv
|
||||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,14 +0,0 @@
|
|||||||
# 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,17 +1,63 @@
|
|||||||
# Qocker-miqt
|
# Qocker
|
||||||
|
|
||||||
Qocker-miqt is a user-friendly GUI application for managing 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.
|
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.
|
||||||
|
|
||||||
## Building
|
## Features
|
||||||
|
|
||||||
```bash
|
- **Container Overview**: View all your Docker containers in a tree-like structure.
|
||||||
apt install qt6-base-dev build-essential golang-go
|
- **Quick Terminal Access**: Open a terminal for any container with a double-click.
|
||||||
go build -ldflags '-s -w'
|
- **Container Management**: Start, stop, and remove containers directly from the GUI.
|
||||||
./qocker-miqt
|
- **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
|
||||||
```
|
```
|
||||||
|
|
||||||
If your `docker` binary requires `sudo`, then
|
3. Navigate to the project directory:
|
||||||
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.
|
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).
|
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 146 KiB |
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
5
go.mod
5
go.mod
@ -1,5 +0,0 @@
|
|||||||
module code.ivysaur.me/qocker-miqt
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
||||||
require github.com/mappu/miqt v0.7.2-0.20250104001511-4c0d782bd34c // indirect
|
|
4
go.sum
4
go.sum
@ -1,4 +0,0 @@
|
|||||||
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
920
main.go
@ -1,920 +0,0 @@
|
|||||||
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
Normal file
664
main.py
Normal file
@ -0,0 +1,664 @@
|
|||||||
|
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
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
pyqt5==5.15.10
|
Loading…
x
Reference in New Issue
Block a user