Browse Source

experiementing with tunnels list gui

ubuntu-nvidia-bug
forest 7 months ago
parent
commit
f3d6673236
7 changed files with 350 additions and 31 deletions
  1. +2
    -1
      .gitignore
  2. +5
    -4
      greenhouse-daemon/child_process_service.go
  3. +314
    -24
      src/main/python/main.py
  4. BIN
      src/main/resources/base/icon_create.png
  5. BIN
      src/main/resources/base/icon_open_socket.png
  6. +29
    -2
      src/main/resources/base/styles.qss
  7. BIN
      src/main/resources/base/tunnel_arrow.png

+ 2
- 1
.gitignore View File

@ -1,3 +1,4 @@
venv
__pycache__
__pycache__
*.kra

+ 5
- 4
greenhouse-daemon/child_process_service.go View File

@ -18,10 +18,11 @@ import (
)
type ChildProcessStatus struct {
Enabled bool
PID int
Started time.Time
CrashLoop bool
Enabled bool `json:"enabled"`
PID int `json:"pid"`
Started time.Time `json:"started"`
CrashLoop bool `json:"crash_loop"`
Healthy bool `json:"healthy"`
}
type ChildProcessLog struct {


+ 314
- 24
src/main/python/main.py View File

@ -1,7 +1,7 @@
from fbs_runtime.application_context.PyQt5 import ApplicationContext
from PyQt5.QtCore import Qt, QThreadPool
from PyQt5.QtGui import QPixmap, QColor
from PyQt5.QtWidgets import QWidget, QLabel, QLineEdit, QPushButton, QFrame, QVBoxLayout, QHBoxLayout, QMessageBox
from PyQt5.QtGui import QPixmap, QColor, QIcon
from PyQt5.QtWidgets import QWidget, QLabel, QLineEdit, QPushButton, QFrame, QVBoxLayout, QHBoxLayout, QMessageBox, QScrollArea, QComboBox, QCheckBox
from qtwidgets import PasswordEdit
from pyqtspinner.spinner import WaitingSpinner
@ -13,6 +13,7 @@ import requests
import urllib3
import time
import sys
import sip
from worker import Worker
@ -25,31 +26,33 @@ def my_exec_info_message(exec_info):
class MainWindow(QWidget):
def __init__(self, logo: str):
def __init__(self, logo: str, icon_create: str, icon_open_socket: str, image_tunnel_arrow: str):
super().__init__()
self.icon_create = icon_create
self.icon_open_socket = icon_open_socket
self.image_tunnel_arrow = image_tunnel_arrow
self.register_button = None
self.tunnels = [dict()]
self.screens = dict(
init= self.create_initial_loading_frame(),
login= self.create_login_frame(logo),
main= self.create_main_frame(),
main= self.create_main_frame(None),
)
vbox = QVBoxLayout()
vbox.addStretch()
self.screens_parent = QVBoxLayout()
for kv in self.screens.items():
vbox.addWidget(kv[1])
self.screens_parent.addWidget(kv[1], 2)
vbox.addStretch()
self.setLayout(vbox)
self.set_active_frame("init")
self.resize(600, 600)
self.setLayout(self.screens_parent)
self.set_active_screen("init")
self.resize(900, 700)
self.get_status(self.get_status_callback)
def set_active_frame(self, name: str):
def set_active_screen(self, name: str):
for kv in self.screens.items():
if kv[0] == name:
kv[1].show()
@ -79,15 +82,38 @@ class MainWindow(QWidget):
else:
if type(status) is dict and "needs_api_token" in status:
if status["needs_api_token"]:
self.set_active_frame("login")
self.set_active_screen("login")
else:
self.set_active_frame("main")
self.update_main_screen(status)
self.set_active_screen("main")
else:
self.error_popup("Internal Error", f"expected a dictionary containing the key 'needs_api_token', instead got {status}")
def update_main_screen(self, status: dict):
pass
# how toremove widget https://stackoverflow.com/questions/5899826/pyqt-how-to-remove-a-widget
self.screens_parent.removeWidget(self.screens["main"])
sip.delete(self.screens["main"])
self.screens["main"] = None
self.screens["main"] = self.create_main_frame(status)
self.screens_parent.addWidget(self.screens["main"])
def update_tunnels_list(self):
# how toremove widget
self.tunnels_frame_layout.removeWidget(self.tunnels_list)
sip.delete(self.tunnels_list)
self.tunnels_list = None
self.tunnels_list = self.create_tunnels_list()
self.tunnels_frame_layout.addWidget(self.tunnels_list)
self.tunnels_frame_layout.setAlignment(self.tunnels_list, Qt.AlignTop)
def create_tunnel(self):
self.tunnels.append(dict())
self.update_tunnels_list()
def register_clicked(self, hostname: str, api_token: str):
self.register_button.setDisabled(True)
@ -183,9 +209,8 @@ class MainWindow(QWidget):
return frame_to_return
def create_login_frame(self, logo: str):
logo_pixmap = QPixmap(logo)
logo_label = QLabel()
logo_label.setPixmap(logo_pixmap)
logo_label.setPixmap(QPixmap(logo))
header_label = QLabel()
header_label.setText("Register this Computer as a Server")
@ -244,26 +269,291 @@ class MainWindow(QWidget):
return frame_to_return
def create_main_frame(self):
def create_main_frame(self, status: dict):
vbox = QVBoxLayout()
loading_label = QLabel()
loading_label.setText("main screen")
vbox.addWidget(loading_label)
vbox.setAlignment(loading_label, Qt.AlignHCenter)
if status is None:
error_label = QLabel()
error_label.setText("no service status provided")
vbox.addWidget(error_label)
frame_to_return = QFrame()
frame_to_return.setLayout(vbox)
return frame_to_return
underlying_services_status_label = QLabel()
underlying_services_status_label.setText("Underlying Services Status:")
vbox.addWidget(underlying_services_status_label)
underlying_services_frame = QFrame()
hbox = QHBoxLayout()
hbox.addWidget(self.create_service_status("Caddy Server", "Automatic HTTPS Certificates via Let's Encrypt", status["caddy"]))
hbox.addWidget(self.create_service_status("Threshold Client", "Greenhouse's Network Tunnel", status["threshold"]))
underlying_services_frame.setLayout(hbox)
vbox.addWidget(underlying_services_frame)
tunnels_label = QLabel()
tunnels_label.setText("Tunnels:")
vbox.addWidget(tunnels_label)
self.tunnels_list = self.create_tunnels_list()
self.tunnels_frame_layout = QVBoxLayout()
self.tunnels_frame_layout.addWidget(self.tunnels_list)
self.tunnels_frame_layout.setAlignment(self.tunnels_list, Qt.AlignTop)
#self.tunnels_frame_layout.addStr
tunnels_frame = QFrame()
tunnels_frame.setLayout(self.tunnels_frame_layout)
scroll_area = QScrollArea()
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(tunnels_frame)
vbox.addWidget(scroll_area, 2)
actions_row = QHBoxLayout()
actions_row.addStretch()
create_tunnel_button = QPushButton('Create Tunnel')
create_tunnel_button.setIcon(QIcon(self.icon_create))
create_tunnel_button.clicked.connect(self.create_tunnel)
actions_row.addWidget(create_tunnel_button)
vbox.addLayout(actions_row)
frame_to_return = QFrame()
frame_to_return.setLayout(vbox)
return frame_to_return
def create_service_status(self, service_name: str, description: str, service: dict) -> QFrame:
vbox = QVBoxLayout()
service_label = QLabel()
service_label.setText(service_name)
service_label.setObjectName("service_label")
vbox.addWidget(service_label)
hbox = QHBoxLayout()
hbox.addSpacing(10)
inner = QVBoxLayout()
hbox.addLayout(inner)
hbox.addStretch()
vbox.addLayout(hbox)
service_description = QLabel()
service_description.setText(description)
service_description.setObjectName("service_description")
inner.addWidget(service_description)
process_status = QLabel()
health_status = QLabel()
#print(service)
if service["enabled"]:
if service["crash_loop"]:
process_status.setText("Process: unable to start (keeps on crashing)")
elif service["pid"] != 0:
process_status.setText("Process: starting")
else:
process_status.setText(f"Process: running (PID: {service['pid']})")
if service["healthy"]:
health_status.setText("Health Check: healthy")
else:
health_status.setText("Health Check: unhealthy")
else:
process_status.setText("Process: stopped (service is disabled)")
health_status.setText("Health Check: N/A (service is disabled)")
inner.addWidget(process_status)
inner.addWidget(health_status)
logs_button = QPushButton('View Logs...')
logs_button.clicked.connect(lambda: print("display logs"))
inner.addWidget(logs_button)
frame_to_return = QFrame()
frame_to_return.setLayout(vbox)
frame_to_return.setObjectName("rounded_border")
return frame_to_return
def create_tunnels_list(self) -> QFrame:
vbox = QVBoxLayout()
for tunnel in self.tunnels:
row = self.create_tunnel_row(tunnel)
vbox.addWidget(row)
vbox.addStretch()
frame_to_return = QFrame()
frame_to_return.setLayout(vbox)
return frame_to_return
def create_tunnel_row(self, tunnel: dict) -> QFrame:
protocols = [
dict(description="HTTPS", key="https"),
dict(description="TLS (any)", key="tls"),
dict(description="TCP (any)", key="tcp"),
]
source_row = QHBoxLayout()
protocol_column = QVBoxLayout()
protocol_label = QLabel()
protocol_label.setText("protocol")
protocol_column.addWidget(protocol_label)
protocol_dropdown = QComboBox()
for protocol in protocols:
protocol_dropdown.addItem(protocol["description"])
for i in range(len(protocols)):
if 'protocol' in tunnel and protocols[i]['key'] == tunnel['protocol']:
protocol_dropdown.setCurrentIndex(i)
def setProtocolOnTunnel(tunnel: dict, protocol: str):
tunnel['protocol'] = protocol
protocol_dropdown.currentIndexChanged.connect(lambda i: setProtocolOnTunnel(tunnel, protocols[i]['key']))
protocol_column.addWidget(protocol_dropdown)
source_row.addLayout(protocol_column)
colon_slash_slash_column = QVBoxLayout()
colon_slash_slash_column.addStretch()
colon_slash_slash_label = QLabel()
colon_slash_slash_label.setText("://")
colon_slash_slash_column.addWidget(colon_slash_slash_label)
colon_slash_slash_column.addSpacing(4)
source_row.addLayout(colon_slash_slash_column)
domain_column = QVBoxLayout()
labels_row = QHBoxLayout()
subdomain_label = QLabel()
subdomain_label.setText("subdomain")
subdomain_checkbox = QCheckBox()
subdomain_checkbox.setChecked(True if 'has_subdomain' in tunnel and tunnel['has_subdomain'] else False)
def setHasSubdomainOnTunnel(tunnel: dict, has_subdomain: bool):
print(has_subdomain)
tunnel['has_subdomain'] = has_subdomain
subdomain_checkbox.stateChanged.connect(lambda i: setHasSubdomainOnTunnel(tunnel, subdomain_checkbox.isChecked()))
labels_row.addWidget(subdomain_label)
labels_row.addWidget(subdomain_checkbox)
labels_row.addStretch()
domain_label = QLabel()
domain_label.setText("domain")
labels_row.addWidget(domain_label)
domain_column.addLayout(labels_row)
fields_row = QHBoxLayout()
subdomain_input = QLineEdit()
subdomain_input.setObjectName("subdomain_input")
dot_label = QLabel()
dot_label.setText(".")
domain_dropdown = QComboBox()
domain_dropdown.setObjectName("domain_dropdown")
domain_dropdown.addItem("forest-n-johnson.greenhouseusers.com")
fields_row.addWidget(subdomain_input)
fields_row.addWidget(dot_label)
fields_row.addWidget(domain_dropdown)
domain_column.addLayout(fields_row)
source_row.addLayout(domain_column)
row = QHBoxLayout()
source_and_sink = QVBoxLayout()
source_and_sink.addLayout(source_row)
sink_row = QHBoxLayout()
sink_row.addStretch()
type_column = QVBoxLayout()
type_label = QLabel()
type_label.setText("type")
type_dropdown = QComboBox()
type_dropdown.addItem("forward to local server")
type_dropdown.addItem("forward to another server")
type_dropdown.addItem("serve local files")
type_column.addWidget(type_label)
type_column.addWidget(type_dropdown)
sink_row.addLayout(type_column)
localport_column = QVBoxLayout()
localport_label = QLabel()
localport_label.setText("forward to local server")
localport_row = QHBoxLayout()
local
choose_listener_button = QPushButton('')
choose_listener_button.setIcon(QIcon(self.icon_open_socket))
def choose_listener_for_tunnel(tunnel: dict):
print("choose_listener_for_tunnel")
choose_listener_button.clicked.connect(lambda: choose_listener_for_tunnel(tunnel))
actions_row.addWidget(choose_listener_button)
localport_column.addWidget(localport_label)
localport_column.addWidget(localport_dropdown)
sink_row.addLayout(localport_column)
source_and_sink.addSpacing(8)
source_and_sink.addLayout(sink_row)
source_and_sink.addSpacing(3)
row.addLayout(source_and_sink)
arrow_column = QVBoxLayout()
arrow_image_label = QLabel()
arrow_image_label.setPixmap(QPixmap(self.image_tunnel_arrow))
arrow_column.addStretch()
arrow_column.addWidget(arrow_image_label)
row.addLayout(arrow_column)
row.addStretch()
to_return = QFrame()
to_return.setLayout(row)
return to_return
if __name__ == '__main__':
app_context = ApplicationContext()
stylesheet = app_context.get_resource('styles.qss')
logo = app_context.get_resource('greenhouse.png')
icon_create = app_context.get_resource('icon_create.png')
icon_open_socket = app_context.get_resource('icon_open_socket.png')
image_tunnel_arrow = app_context.get_resource('tunnel_arrow.png')
app_context.app.setStyleSheet(open(stylesheet).read())
window = MainWindow(logo)
window = MainWindow(logo, icon_create, icon_open_socket, image_tunnel_arrow)
window.show()
window.resize(600, 600)
window.resize(900, 700)
exit_code = app_context.app.exec_()
sys.exit(exit_code)

BIN
src/main/resources/base/icon_create.png View File

Before After
Width: 72  |  Height: 72  |  Size: 9.6 KiB

BIN
src/main/resources/base/icon_open_socket.png View File

Before After
Width: 128  |  Height: 128  |  Size: 10 KiB

+ 29
- 2
src/main/resources/base/styles.qss View File

@ -14,6 +14,16 @@ QLabel#header_label {
font-size: 13pt;
}
QFrame#rounded_border {
border-radius: 5;
border: 1px solid gray;
}
QLabel#service_label {
font-weight: bold;
}
QLabel#service_description {
font-style: italic;
}
QLineEdit {
@ -21,5 +31,22 @@ QLineEdit {
}
QPushButton {
max-width: 10em;
}
max-width: 10em;
}
QLineEdit#subdomain_input {
width: 10em;
}
QComboBox#domain_dropdown {
max-width: 25em;
}
QBoxLayout{
padding:0;
margin:0;
}
QFrame {
padding:0;
margin:0;
}

BIN
src/main/resources/base/tunnel_arrow.png View File

Before After
Width: 43  |  Height: 85  |  Size: 9.0 KiB

Loading…
Cancel
Save