Compare commits
33 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 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1 @@
|
|||||||
/.idea
|
qocker-miqt
|
||||||
/venv
|
|
||||||
.flatpak-builder
|
|
||||||
|
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.
|
```bash
|
||||||
- **Quick Terminal Access**: Open a terminal for any container with a double-click.
|
apt install qt6-base-dev build-essential golang-go
|
||||||
- **Container Management**: Start, stop, and remove containers directly from the GUI.
|
go build -ldflags '-s -w'
|
||||||
- **Real-time Updates**: Container statuses are updated in real-time.
|
./qocker-miqt
|
||||||
- **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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Navigate to the project directory:
|
If your `docker` binary requires `sudo`, then
|
||||||
```
|
1. Run `sudo docker` once to prime the sudo login cache; then
|
||||||
cd qocker
|
2. Run `./qocker-miqt --sudo`. Then qocker will use sudo for all docker invocations.
|
||||||
```
|
|
||||||
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).
|
|
||||||
|
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 |
@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
"app-id": "sa.sy.Qocker",
|
|
||||||
"runtime": "org.kde.Platform",
|
|
||||||
"runtime-version": "5.15",
|
|
||||||
"sdk": "org.kde.Sdk",
|
|
||||||
"command": "qocker",
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"name": "PyQt5",
|
|
||||||
"cleanup": [
|
|
||||||
"/bin/sip",
|
|
||||||
"/include",
|
|
||||||
"/lib/python3.9/site-packages/*.pyi"
|
|
||||||
],
|
|
||||||
"config-opts": [
|
|
||||||
"--disable-static",
|
|
||||||
"--enable-x11"
|
|
||||||
],
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"python configure.py --confirm-license --no-docstrings --assume-shared --no-sip-files --no-qml-plugin --no-tools --no-qsci-api -d ${FLATPAK_DEST}/lib/python3.9/site-packages --sip=${FLATPAK_DEST}/bin/sip --sip-incdir=${FLATPAK_DEST}/include --stubsdir=${FLATPAK_DEST}/lib/python3.9/site-packages --disable=QtSensors --disable=QtWebEngine --disable=QtQuick --disable=QtQml --disable=QtWebChannel --disable=QtWebEngineCore --disable=QWebEngineWidgets --disable=QtQuickWidgets --disable=QtSql --disable=QtXmlPatterns --disable=QtMultimedia --disable=QtMultimediaWidgets --disable=QtLocation --disable=QtDesigner --disable=QtOpenGL --disable=QtBluetooth --disable=QtWebKit --disable=QtWebKitWidgets --disable=QtNfc --disable=QtPositioning",
|
|
||||||
"make -j $FLATPAK_BUILDER_N_JOBS",
|
|
||||||
"make install"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "archive",
|
|
||||||
"url": "https://pypi.io/packages/source/P/PyQt5/PyQt5-5.15.4.tar.gz",
|
|
||||||
"sha256": "2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"name": "sip",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"python configure.py --sip-module PyQt5.sip -b ${FLATPAK_DEST}/bin -d ${FLATPAK_DEST}/lib/python3.9/site-packages -e ${FLATPAK_DEST}/include -v ${FLATPAK_DEST}/share/sip --stubsdir=${FLATPAK_DEST}/lib/python3.9/site-packages",
|
|
||||||
"make",
|
|
||||||
"make install"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "archive",
|
|
||||||
"url": "https://www.riverbankcomputing.com/static/Downloads/sip/4.19.25/sip-4.19.25.tar.gz",
|
|
||||||
"sha256": "b39d93e937647807bac23579edbff25fe46d16213f708370072574ab1f1b4211"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "qocker",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"install -Dm755 main.py /app/bin/qocker"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/xlmnxp/qocker.git",
|
|
||||||
"tag": "v0.2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"finish-args": [
|
|
||||||
"--share=network",
|
|
||||||
"--socket=x11",
|
|
||||||
"--socket=pulseaudio",
|
|
||||||
"--env=QT_QPA_PLATFORM=xcb"
|
|
||||||
]
|
|
||||||
}
|
|
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())
|
||||||
|
}
|
665
main.py
665
main.py
@ -1,665 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
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