stuff
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
from .host_item import HostItem
|
||||
from .location_card import ActiveLocationCard, InactiveLocationCard
|
||||
from .customer_card import ActiveCustomerCard, InactiveCustomerCard
|
||||
from .active_customer_card import ActiveCustomerCard
|
||||
from .inactive_customer_card import InactiveCustomerCard
|
||||
|
||||
__all__ = [
|
||||
'HostItem',
|
||||
'ActiveLocationCard',
|
||||
'ActiveLocationCard',
|
||||
'InactiveLocationCard',
|
||||
'ActiveCustomerCard',
|
||||
'InactiveCustomerCard'
|
||||
]
|
||||
]
|
||||
|
||||
399
widgets/active_customer_card.py
Normal file
399
widgets/active_customer_card.py
Normal file
@@ -0,0 +1,399 @@
|
||||
from utils import IconLoader
|
||||
from gi.repository import Gtk
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
|
||||
def escape_markup(text: str) -> str:
|
||||
"""Escape special characters for Pango markup."""
|
||||
return text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
class ActiveCustomerCard:
|
||||
def __init__(self, customer, callbacks):
|
||||
self.customer = customer
|
||||
self.callbacks = callbacks
|
||||
self.expanded = True # Start expanded by default
|
||||
self.location_expanded = {} # Track expansion state of each location
|
||||
# Initialize all locations as expanded
|
||||
for location in self.customer.locations:
|
||||
self.location_expanded[location.name] = True
|
||||
self.widget = self._create_widget()
|
||||
|
||||
def _create_widget(self):
|
||||
# Customer card container
|
||||
customer_frame = Gtk.Frame()
|
||||
customer_frame.get_style_context().add_class("card")
|
||||
customer_frame.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
|
||||
customer_vbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
customer_frame.add(customer_vbox)
|
||||
|
||||
# Customer header row
|
||||
customer_row = self._create_customer_header()
|
||||
customer_vbox.pack_start(customer_row, False, False, 0)
|
||||
|
||||
# Content container (locations)
|
||||
self.content_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
self.content_box.set_margin_start(8)
|
||||
self.content_box.set_margin_end(8)
|
||||
self.content_box.set_margin_bottom(8)
|
||||
customer_vbox.pack_start(self.content_box, False, False, 0)
|
||||
|
||||
# Add location cards
|
||||
for location in self.customer.locations:
|
||||
location_card = self._create_location_card(location)
|
||||
self.content_box.pack_start(location_card, False, False, 0)
|
||||
|
||||
return customer_frame
|
||||
|
||||
def _create_customer_header(self):
|
||||
"""Create the customer header row."""
|
||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
row.set_margin_start(4)
|
||||
row.set_margin_end(4)
|
||||
row.set_margin_top(4)
|
||||
row.set_margin_bottom(2)
|
||||
|
||||
# Expand/collapse arrow
|
||||
self.expand_button = Gtk.Button()
|
||||
self.expand_button.set_relief(Gtk.ReliefStyle.NONE)
|
||||
self.expand_button.set_can_focus(False)
|
||||
self.expand_button.set_size_request(20, 20)
|
||||
self._update_expand_button()
|
||||
self.expand_button.connect("clicked", self._on_expand_toggle)
|
||||
row.pack_start(self.expand_button, False, False, 0)
|
||||
|
||||
# Customer name
|
||||
customer_label = Gtk.Label()
|
||||
escaped_name = escape_markup(self.customer.name)
|
||||
customer_label.set_markup(f"<b>{escaped_name}</b>")
|
||||
customer_label.set_halign(Gtk.Align.START)
|
||||
row.pack_start(customer_label, True, True, 0)
|
||||
|
||||
# Customer service icons (right side)
|
||||
self._add_customer_service_icons(row)
|
||||
|
||||
return row
|
||||
|
||||
def _create_location_card(self, location):
|
||||
"""Create a location card (subcard within customer card)."""
|
||||
# Location subcard
|
||||
location_frame = Gtk.Frame()
|
||||
location_frame.get_style_context().add_class("card")
|
||||
location_frame.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
|
||||
location_vbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
location_frame.add(location_vbox)
|
||||
|
||||
# Location header row
|
||||
location_row = self._create_location_header(location)
|
||||
location_vbox.pack_start(location_row, False, False, 0)
|
||||
|
||||
# Hosts container (collapsible)
|
||||
location_key = location.name
|
||||
hosts_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
hosts_box.set_margin_start(8)
|
||||
hosts_box.set_margin_end(4)
|
||||
hosts_box.set_margin_bottom(4)
|
||||
|
||||
# Add hosts for this location
|
||||
if location.hosts:
|
||||
for host in location.hosts:
|
||||
host_row = self._create_host_row(host, location)
|
||||
hosts_box.pack_start(host_row, False, False, 0)
|
||||
|
||||
# Add sub-hosts (VMs)
|
||||
if host.sub_hosts:
|
||||
for vm in host.sub_hosts:
|
||||
vm_row = self._create_host_row(
|
||||
vm, location, is_vm=True)
|
||||
hosts_box.pack_start(vm_row, False, False, 0)
|
||||
|
||||
location_vbox.pack_start(hosts_box, False, False, 0)
|
||||
|
||||
# Store reference to hosts_box for expand/collapse
|
||||
setattr(location_frame, 'hosts_box', hosts_box)
|
||||
setattr(location_frame, 'location', location)
|
||||
|
||||
# Set initial visibility
|
||||
expanded = self.location_expanded.get(location_key, True)
|
||||
hosts_box.set_visible(expanded)
|
||||
|
||||
return location_frame
|
||||
|
||||
def _create_location_header(self, location):
|
||||
"""Create the location header row."""
|
||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
row.set_margin_start(4)
|
||||
row.set_margin_end(4)
|
||||
row.set_margin_top(4)
|
||||
row.set_margin_bottom(2)
|
||||
|
||||
# Location expand/collapse arrow
|
||||
expand_btn = Gtk.Button()
|
||||
expand_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
expand_btn.set_can_focus(False)
|
||||
expand_btn.set_size_request(20, 20)
|
||||
|
||||
# Set initial arrow direction
|
||||
expanded = self.location_expanded.get(location.name, True)
|
||||
expand_btn.set_label("▼" if expanded else "▶")
|
||||
|
||||
# Connect to toggle function
|
||||
expand_btn.connect(
|
||||
"clicked", lambda btn: self._toggle_location_expansion(location, btn))
|
||||
row.pack_start(expand_btn, False, False, 0)
|
||||
|
||||
# Location info
|
||||
location_info = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
# Location name
|
||||
name_label = Gtk.Label()
|
||||
escaped_location_name = escape_markup(location.name)
|
||||
name_label.set_markup(f"<b>{escaped_location_name}</b>")
|
||||
name_label.set_halign(Gtk.Align.START)
|
||||
location_info.pack_start(name_label, False, False, 0)
|
||||
|
||||
# External addresses (small text)
|
||||
if location.external_addresses:
|
||||
addr_text = ", ".join(
|
||||
location.external_addresses[:2]) # Show first 2
|
||||
if len(location.external_addresses) > 2:
|
||||
addr_text += f" (+{len(location.external_addresses) - 2} more)"
|
||||
addr_label = Gtk.Label()
|
||||
addr_label.set_markup(f"<small><i>{addr_text}</i></small>")
|
||||
addr_label.set_halign(Gtk.Align.START)
|
||||
location_info.pack_start(addr_label, False, False, 0)
|
||||
|
||||
row.pack_start(location_info, True, True, 0)
|
||||
|
||||
# VPN Status
|
||||
status_label = Gtk.Label()
|
||||
if location.connected:
|
||||
status_label.set_markup(
|
||||
"<span color='#4caf50'><b>Connected</b></span>")
|
||||
else:
|
||||
status_label.set_markup(
|
||||
"<span color='#999'><b>Disconnected</b></span>")
|
||||
row.pack_start(status_label, False, False, 0)
|
||||
|
||||
# Action icons
|
||||
self._add_location_action_icons(row, location)
|
||||
|
||||
return row
|
||||
|
||||
def _create_host_row(self, host, location, is_vm=False):
|
||||
"""Create a host row with aligned IP addresses."""
|
||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
margin = 16 if not is_vm else 32 # VMs are more indented
|
||||
row.set_margin_start(margin)
|
||||
row.set_margin_end(4)
|
||||
row.set_margin_top(1)
|
||||
row.set_margin_bottom(1)
|
||||
|
||||
# Host icon - custom or fallback to Material Icons
|
||||
icon_widget = IconLoader.get_host_icon_widget(host, size=20)
|
||||
icon_container = Gtk.Box()
|
||||
icon_container.set_size_request(24, 24) # Fixed size for alignment
|
||||
icon_container.set_center_widget(icon_widget)
|
||||
row.pack_start(icon_container, False, False, 0)
|
||||
|
||||
# Host name (fixed width for alignment)
|
||||
escaped_host_name = escape_markup(host.name)
|
||||
name_markup = f"<i>{escaped_host_name}</i>" if is_vm else f"<b>{escaped_host_name}</b>"
|
||||
name_label = Gtk.Label()
|
||||
name_label.set_markup(name_markup)
|
||||
name_label.set_halign(Gtk.Align.START)
|
||||
name_label.set_size_request(150, -1) # Fixed width to align IPs
|
||||
row.pack_start(name_label, False, False, 0)
|
||||
|
||||
# IP address (aligned in middle)
|
||||
ip_label = Gtk.Label()
|
||||
ip_label.set_markup(f"<small>{host.get_ip_display()}</small>")
|
||||
ip_label.set_size_request(120, -1) # Fixed width for alignment
|
||||
ip_label.set_halign(Gtk.Align.CENTER)
|
||||
ip_label.set_tooltip_text(", ".join(host.get_all_ips()) if len(
|
||||
host.ip_addresses) > 1 else host.get_primary_ip())
|
||||
row.pack_start(ip_label, False, False, 0)
|
||||
|
||||
# Spacer to push service icons to the right
|
||||
spacer = Gtk.Box()
|
||||
row.pack_start(spacer, True, True, 0)
|
||||
|
||||
# Service action icons
|
||||
self._add_host_service_icons(row, host, location)
|
||||
|
||||
return row
|
||||
|
||||
def _add_customer_service_icons(self, row):
|
||||
"""Add customer service icons to the right side."""
|
||||
# Service type to icon mapping
|
||||
service_icons = {
|
||||
'Email & Office': '📧', # O365
|
||||
'Phone System': '📞', # PBX
|
||||
'CRM': '👥', # Salesforce
|
||||
'Email': '📧',
|
||||
'Office': '📄',
|
||||
}
|
||||
|
||||
# Add icons for each service
|
||||
for service in self.customer.services[:4]: # Limit to 4 icons
|
||||
icon = service_icons.get(service.service_type, '🌐')
|
||||
|
||||
btn = Gtk.Button()
|
||||
btn.set_label(icon)
|
||||
btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
btn.set_can_focus(False)
|
||||
btn.set_size_request(24, 24)
|
||||
btn.set_tooltip_text(f"Open {service.name}")
|
||||
btn.connect("clicked", lambda b,
|
||||
s=service: self.callbacks['open_customer_service'](s))
|
||||
row.pack_start(btn, False, False, 0)
|
||||
|
||||
# Menu button (always last)
|
||||
menu_btn = self._create_menu_button()
|
||||
row.pack_start(menu_btn, False, False, 0)
|
||||
|
||||
def _add_location_action_icons(self, row, location):
|
||||
"""Add location action icons."""
|
||||
# Connection toggle
|
||||
connect_icon = "🔌" if not location.connected else "🔓"
|
||||
connect_btn = Gtk.Button()
|
||||
connect_btn.set_label(connect_icon)
|
||||
connect_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
connect_btn.set_can_focus(False)
|
||||
connect_btn.set_size_request(24, 24)
|
||||
tooltip = "Connect to VPN" if not location.connected else "Disconnect VPN"
|
||||
connect_btn.set_tooltip_text(tooltip)
|
||||
connect_btn.connect(
|
||||
"clicked", lambda b: self.callbacks['toggle_connection'](location))
|
||||
row.pack_start(connect_btn, False, False, 0)
|
||||
|
||||
# Refresh/reload
|
||||
refresh_btn = Gtk.Button()
|
||||
refresh_btn.set_label("🔄")
|
||||
refresh_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
refresh_btn.set_can_focus(False)
|
||||
refresh_btn.set_size_request(24, 24)
|
||||
refresh_btn.set_tooltip_text("Refresh connection")
|
||||
row.pack_start(refresh_btn, False, False, 0)
|
||||
|
||||
# Menu
|
||||
menu_btn = self._create_menu_button()
|
||||
row.pack_start(menu_btn, False, False, 0)
|
||||
|
||||
def _add_host_service_icons(self, row, host, location):
|
||||
"""Add host service icons with reachability check."""
|
||||
# Service type to Material Icons mapping
|
||||
# Icon names from: https://fonts.google.com/icons
|
||||
service_icons = {
|
||||
'SSH': 'terminal', # Terminal icon for SSH
|
||||
'Web GUI': 'language', # Globe icon for web
|
||||
'RDP': 'desktop_windows', # Desktop icon for RDP
|
||||
'VNC': 'monitor', # Monitor icon for VNC
|
||||
'SMB': 'folder_shared', # Shared folder for SMB
|
||||
'Database': 'storage', # Database/storage icon
|
||||
'FTP': 'cloud_upload' # Upload icon for FTP
|
||||
}
|
||||
|
||||
# Add icons for services
|
||||
for service in host.services[:3]: # Limit to 3 service icons
|
||||
# Default to settings icon
|
||||
icon = service_icons.get(service.service_type.value, 'settings')
|
||||
|
||||
# Check if service is reachable
|
||||
is_reachable = location.is_service_reachable(host, service)
|
||||
is_external = location.get_external_url_for_service(
|
||||
host, service) is not None
|
||||
|
||||
btn = Gtk.Button()
|
||||
btn.set_label(icon) # Material Icons uses ligatures
|
||||
btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
btn.set_can_focus(False)
|
||||
btn.set_size_request(24, 24)
|
||||
|
||||
# Apply color styling based on reachability
|
||||
if is_reachable:
|
||||
# Green for accessible
|
||||
btn.get_style_context().add_class("service-icon-accessible")
|
||||
if is_external and not location.connected:
|
||||
external_url = location.get_external_url_for_service(
|
||||
host, service)
|
||||
btn.set_tooltip_text(
|
||||
f"{service.service_type.value}: {service.name}\nExternal: {external_url}")
|
||||
else:
|
||||
btn.set_tooltip_text(
|
||||
f"{service.service_type.value}: {service.name}")
|
||||
else:
|
||||
# Red for not accessible
|
||||
btn.get_style_context().add_class("service-icon-inaccessible")
|
||||
btn.set_tooltip_text(
|
||||
f"{service.service_type.value}: {service.name}\nNot reachable (VPN disconnected)")
|
||||
|
||||
# Enable/disable based on reachability
|
||||
btn.set_sensitive(is_reachable)
|
||||
|
||||
# Connect click handler only if reachable
|
||||
if is_reachable:
|
||||
btn.connect("clicked", lambda b,
|
||||
s=service: self.callbacks['open_service'](s))
|
||||
|
||||
row.pack_start(btn, False, False, 0)
|
||||
|
||||
# Menu button
|
||||
menu_btn = self._create_menu_button()
|
||||
row.pack_start(menu_btn, False, False, 0)
|
||||
|
||||
def _create_menu_button(self):
|
||||
"""Create a menu button with empty popup."""
|
||||
menu_btn = Gtk.MenuButton()
|
||||
menu_btn.set_label("⋯") # Three dots menu
|
||||
menu_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
menu_btn.set_can_focus(False)
|
||||
menu_btn.set_size_request(24, 24)
|
||||
|
||||
# Create empty menu for now
|
||||
menu = Gtk.Menu()
|
||||
placeholder_item = Gtk.MenuItem(label="(Empty menu)")
|
||||
placeholder_item.set_sensitive(False)
|
||||
menu.append(placeholder_item)
|
||||
menu.show_all()
|
||||
|
||||
menu_btn.set_popup(menu)
|
||||
return menu_btn
|
||||
|
||||
def _update_expand_button(self):
|
||||
"""Update the expand button arrow direction."""
|
||||
if self.expanded:
|
||||
self.expand_button.set_label("▼")
|
||||
else:
|
||||
self.expand_button.set_label("▶")
|
||||
|
||||
def _toggle_location_expansion(self, location, button):
|
||||
"""Toggle the expansion state of a specific location."""
|
||||
location_key = location.name
|
||||
current_state = self.location_expanded.get(location_key, True)
|
||||
new_state = not current_state
|
||||
self.location_expanded[location_key] = new_state
|
||||
|
||||
# Update button arrow
|
||||
button.set_label("▼" if new_state else "▶")
|
||||
|
||||
# Find the location card and toggle its hosts box visibility
|
||||
for widget in self.content_box.get_children():
|
||||
if hasattr(widget, 'location') and widget.location.name == location_key:
|
||||
hosts_box = getattr(widget, 'hosts_box', None)
|
||||
if hosts_box:
|
||||
hosts_box.set_visible(new_state)
|
||||
break
|
||||
|
||||
def _on_expand_toggle(self, button):
|
||||
"""Toggle the expanded state."""
|
||||
self.expanded = not self.expanded
|
||||
self._update_expand_button()
|
||||
self.content_box.set_visible(self.expanded)
|
||||
@@ -1,114 +0,0 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from .location_card import ActiveLocationCard, InactiveLocationCard
|
||||
|
||||
|
||||
class ActiveCustomerCard:
|
||||
def __init__(self, customer, callbacks):
|
||||
self.customer = customer
|
||||
self.callbacks = callbacks
|
||||
self.widget = self._create_widget()
|
||||
|
||||
def _create_widget(self):
|
||||
# GNOME-style card container
|
||||
card_frame = Gtk.Frame()
|
||||
card_frame.get_style_context().add_class("card")
|
||||
card_frame.set_shadow_type(Gtk.ShadowType.NONE) # Shadow handled by CSS
|
||||
|
||||
card_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
card_frame.add(card_vbox)
|
||||
|
||||
# Customer header
|
||||
customer_label = Gtk.Label()
|
||||
customer_label.set_markup(f"<b><big>🏢 {self.customer.name}</big></b>")
|
||||
customer_label.set_halign(Gtk.Align.START)
|
||||
card_vbox.pack_start(customer_label, False, False, 0)
|
||||
|
||||
# Customer services section
|
||||
if self.customer.services:
|
||||
services_label = Gtk.Label()
|
||||
services_label.set_markup("<b>Cloud Services</b>")
|
||||
services_label.set_halign(Gtk.Align.START)
|
||||
services_label.set_margin_top(8)
|
||||
card_vbox.pack_start(services_label, False, False, 0)
|
||||
|
||||
# Services box with indent
|
||||
services_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
services_box.set_margin_start(16)
|
||||
services_box.set_margin_bottom(8)
|
||||
card_vbox.pack_start(services_box, False, False, 0)
|
||||
|
||||
for service in self.customer.services:
|
||||
service_btn = Gtk.Button(label=service.name)
|
||||
service_btn.get_style_context().add_class("suggested-action")
|
||||
service_btn.connect("clicked", lambda btn, s=service: self.callbacks['open_customer_service'](s))
|
||||
services_box.pack_start(service_btn, False, False, 0)
|
||||
|
||||
# Locations section
|
||||
for i, location in enumerate(self.customer.locations):
|
||||
if i > 0: # Add separator between locations
|
||||
separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
separator.set_margin_top(8)
|
||||
separator.set_margin_bottom(8)
|
||||
card_vbox.pack_start(separator, False, False, 0)
|
||||
|
||||
location_card = ActiveLocationCard(location, self.customer.name, self.callbacks)
|
||||
card_vbox.pack_start(location_card.widget, False, False, 0)
|
||||
|
||||
return card_frame
|
||||
|
||||
|
||||
class InactiveCustomerCard:
|
||||
def __init__(self, customer, callbacks):
|
||||
self.customer = customer
|
||||
self.callbacks = callbacks
|
||||
self.widget = self._create_widget()
|
||||
|
||||
def _create_widget(self):
|
||||
# GNOME-style card container
|
||||
card_frame = Gtk.Frame()
|
||||
card_frame.get_style_context().add_class("card")
|
||||
card_frame.set_shadow_type(Gtk.ShadowType.NONE) # Shadow handled by CSS
|
||||
|
||||
card_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
card_frame.add(card_vbox)
|
||||
|
||||
# Customer header - muted
|
||||
customer_label = Gtk.Label()
|
||||
customer_label.set_markup(f"<span alpha='60%'><b><big>🏢 {self.customer.name}</big></b></span>")
|
||||
customer_label.set_halign(Gtk.Align.START)
|
||||
card_vbox.pack_start(customer_label, False, False, 0)
|
||||
|
||||
# Customer services section - list format for inactive
|
||||
if self.customer.services:
|
||||
services_label = Gtk.Label()
|
||||
services_label.set_markup("<span alpha='60%'><b>Cloud Services</b></span>")
|
||||
services_label.set_halign(Gtk.Align.START)
|
||||
services_label.set_margin_top(8)
|
||||
card_vbox.pack_start(services_label, False, False, 0)
|
||||
|
||||
# Services list with indent
|
||||
services_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
services_vbox.set_margin_start(16)
|
||||
services_vbox.set_margin_bottom(8)
|
||||
card_vbox.pack_start(services_vbox, False, False, 0)
|
||||
|
||||
for service in self.customer.services:
|
||||
service_label = Gtk.Label()
|
||||
service_label.set_markup(f"<span alpha='60%'><small>• {service.name} ({service.service_type})</small></span>")
|
||||
service_label.set_halign(Gtk.Align.START)
|
||||
services_vbox.pack_start(service_label, False, False, 0)
|
||||
|
||||
# Locations section
|
||||
for i, location in enumerate(self.customer.locations):
|
||||
if i > 0: # Add separator between locations
|
||||
separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
separator.set_margin_top(8)
|
||||
separator.set_margin_bottom(8)
|
||||
card_vbox.pack_start(separator, False, False, 0)
|
||||
|
||||
location_card = InactiveLocationCard(location, self.customer.name, self.callbacks)
|
||||
card_vbox.pack_start(location_card.widget, False, False, 0)
|
||||
|
||||
return card_frame
|
||||
@@ -2,11 +2,18 @@ import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from models import ServiceType, HostType
|
||||
from utils import IconLoader
|
||||
|
||||
|
||||
def escape_markup(text: str) -> str:
|
||||
"""Escape special characters for Pango markup."""
|
||||
return text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
class HostItem:
|
||||
def __init__(self, host, open_service_callback):
|
||||
def __init__(self, host, location, open_service_callback):
|
||||
self.host = host
|
||||
self.location = location
|
||||
self.open_service_callback = open_service_callback
|
||||
self.widget = self._create_widget()
|
||||
|
||||
@@ -18,20 +25,12 @@ class HostItem:
|
||||
host_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
host_box.pack_start(host_header, False, False, 0)
|
||||
|
||||
# Host type icon
|
||||
type_icons = {
|
||||
HostType.LINUX: "🐧",
|
||||
HostType.WINDOWS: "🪟",
|
||||
HostType.WINDOWS_SERVER: "🖥️",
|
||||
HostType.PROXMOX: "📦",
|
||||
HostType.ESXI: "📦",
|
||||
HostType.ROUTER: "🌐",
|
||||
HostType.SWITCH: "🔗"
|
||||
}
|
||||
icon = type_icons.get(self.host.host_type, "💻")
|
||||
|
||||
icon_label = Gtk.Label(label=icon)
|
||||
host_header.pack_start(icon_label, False, False, 0)
|
||||
# Host icon - custom or fallback to Material Icons
|
||||
icon_widget = IconLoader.get_host_icon_widget(self.host, size=24)
|
||||
icon_container = Gtk.Box()
|
||||
icon_container.set_size_request(32, 24) # Fixed size
|
||||
icon_container.set_center_widget(icon_widget)
|
||||
host_header.pack_start(icon_container, False, False, 0)
|
||||
|
||||
# Host details - compact single line
|
||||
details_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1)
|
||||
@@ -39,8 +38,14 @@ class HostItem:
|
||||
|
||||
# Host name with IP inline
|
||||
name_label = Gtk.Label()
|
||||
name_label.set_markup(f"<b>{self.host.name}</b> <small>({self.host.host_type.value}) - <tt>{self.host.ip_address}</tt></small>")
|
||||
ip_display = self.host.get_ip_display()
|
||||
escaped_host_name = escape_markup(self.host.name)
|
||||
escaped_host_type = escape_markup(self.host.host_type.value)
|
||||
escaped_ip_display = escape_markup(ip_display)
|
||||
name_label.set_markup(f"<b>{escaped_host_name}</b> <small>({escaped_host_type}) - <tt>{escaped_ip_display}</tt></small>")
|
||||
name_label.set_halign(Gtk.Align.START)
|
||||
if len(self.host.ip_addresses) > 1:
|
||||
name_label.set_tooltip_text(f"All IPs: {', '.join(self.host.get_all_ips())}")
|
||||
details_vbox.pack_start(name_label, False, False, 0)
|
||||
|
||||
# Services section - compact button row
|
||||
@@ -52,9 +57,35 @@ class HostItem:
|
||||
|
||||
for service in self.host.services:
|
||||
if service.service_type in [ServiceType.WEB_GUI, ServiceType.SSH, ServiceType.RDP]: # Only show launchable services
|
||||
# Check if service is reachable
|
||||
is_reachable = self.location.is_service_reachable(self.host, service)
|
||||
is_external = self.location.get_external_url_for_service(self.host, service) is not None
|
||||
|
||||
service_btn = Gtk.Button(label=service.service_type.value)
|
||||
service_btn.get_style_context().add_class("suggested-action")
|
||||
service_btn.connect("clicked", lambda btn, s=service: self._on_service_clicked(s))
|
||||
|
||||
# Apply color-based styling
|
||||
if is_reachable:
|
||||
# Green styling for accessible services
|
||||
service_btn.get_style_context().add_class("suggested-action")
|
||||
service_btn.set_name("service-btn-accessible")
|
||||
if is_external and not self.location.connected:
|
||||
external_url = self.location.get_external_url_for_service(self.host, service)
|
||||
service_btn.set_tooltip_text(f"Open {service.name}\nExternal: {external_url}")
|
||||
else:
|
||||
service_btn.set_tooltip_text(f"Open {service.name}")
|
||||
else:
|
||||
# Red styling for inaccessible services
|
||||
service_btn.get_style_context().add_class("destructive-action")
|
||||
service_btn.set_name("service-btn-inaccessible")
|
||||
service_btn.set_tooltip_text(f"{service.name} - Not reachable (VPN disconnected)")
|
||||
|
||||
# Enable/disable based on reachability
|
||||
service_btn.set_sensitive(is_reachable)
|
||||
|
||||
# Connect handler only if reachable
|
||||
if is_reachable:
|
||||
service_btn.connect("clicked", lambda btn, s=service: self._on_service_clicked(s))
|
||||
|
||||
services_box.pack_start(service_btn, False, False, 0)
|
||||
|
||||
# Sub-hosts (VMs) section
|
||||
@@ -71,7 +102,7 @@ class HostItem:
|
||||
host_box.pack_start(subhosts_box, False, False, 0)
|
||||
|
||||
for subhost in self.host.sub_hosts:
|
||||
subhost_item = HostItem(subhost, self.open_service_callback)
|
||||
subhost_item = HostItem(subhost, self.location, self.open_service_callback)
|
||||
subhosts_box.pack_start(subhost_item.widget, False, False, 0)
|
||||
|
||||
return host_box
|
||||
|
||||
118
widgets/inactive_customer_card.py
Normal file
118
widgets/inactive_customer_card.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from .location_card import InactiveLocationCard
|
||||
from gi.repository import Gtk
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
|
||||
def escape_markup(text: str) -> str:
|
||||
"""Escape special characters for Pango markup."""
|
||||
return text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
class InactiveCustomerCard:
|
||||
def __init__(self, customer, callbacks):
|
||||
self.customer = customer
|
||||
self.callbacks = callbacks
|
||||
self.expanded = False # Start collapsed by default for inactive
|
||||
self.widget = self._create_widget()
|
||||
|
||||
def _create_widget(self):
|
||||
# GNOME-style card container
|
||||
card_frame = Gtk.Frame()
|
||||
card_frame.get_style_context().add_class("card")
|
||||
card_frame.set_shadow_type(
|
||||
Gtk.ShadowType.NONE) # Shadow handled by CSS
|
||||
|
||||
card_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
card_frame.add(card_vbox)
|
||||
|
||||
# Customer header with expand/collapse button - muted
|
||||
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
card_vbox.pack_start(header_box, False, False, 0)
|
||||
|
||||
# Expand/collapse arrow button
|
||||
self.expand_button = Gtk.Button()
|
||||
self.expand_button.set_relief(Gtk.ReliefStyle.NONE)
|
||||
self.expand_button.set_can_focus(False)
|
||||
self._update_expand_button()
|
||||
self.expand_button.connect("clicked", self._on_expand_toggle)
|
||||
header_box.pack_start(self.expand_button, False, False, 0)
|
||||
|
||||
# Customer name - muted
|
||||
customer_label = Gtk.Label()
|
||||
escaped_name = escape_markup(self.customer.name)
|
||||
customer_label.set_markup(
|
||||
f"<span alpha='60%'><b><big>🏢 {escaped_name}</big></b></span>")
|
||||
customer_label.set_halign(Gtk.Align.START)
|
||||
header_box.pack_start(customer_label, False, False, 0)
|
||||
|
||||
# Location count badge
|
||||
inactive_count = len(self.customer.locations)
|
||||
if inactive_count > 0:
|
||||
count_label = Gtk.Label()
|
||||
count_label.set_markup(
|
||||
f"<span alpha='60%'><small><b>({inactive_count})</b></small></span>")
|
||||
header_box.pack_start(count_label, False, False, 0)
|
||||
|
||||
# Content container (collapsible)
|
||||
self.content_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
self.content_box.set_visible(self.expanded) # Start hidden
|
||||
card_vbox.pack_start(self.content_box, False, False, 0)
|
||||
|
||||
# Customer services section - list format for inactive
|
||||
if self.customer.services:
|
||||
services_label = Gtk.Label()
|
||||
services_label.set_markup(
|
||||
"<span alpha='60%'><b>Cloud Services</b></span>")
|
||||
services_label.set_halign(Gtk.Align.START)
|
||||
services_label.set_margin_top(8)
|
||||
self.content_box.pack_start(services_label, False, False, 0)
|
||||
|
||||
# Services list with indent
|
||||
services_vbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
services_vbox.set_margin_start(16)
|
||||
services_vbox.set_margin_bottom(8)
|
||||
self.content_box.pack_start(services_vbox, False, False, 0)
|
||||
|
||||
for service in self.customer.services:
|
||||
service_label = Gtk.Label()
|
||||
# Escape special characters in markup text
|
||||
escaped_name = escape_markup(service.name)
|
||||
escaped_type = escape_markup(service.service_type)
|
||||
service_label.set_markup(
|
||||
f"<span alpha='60%'><small>• {escaped_name} ({escaped_type})</small></span>")
|
||||
service_label.set_halign(Gtk.Align.START)
|
||||
services_vbox.pack_start(service_label, False, False, 0)
|
||||
|
||||
# Locations section
|
||||
for i, location in enumerate(self.customer.locations):
|
||||
if i > 0: # Add separator between locations
|
||||
separator = Gtk.Separator(
|
||||
orientation=Gtk.Orientation.HORIZONTAL)
|
||||
separator.set_margin_top(8)
|
||||
separator.set_margin_bottom(8)
|
||||
self.content_box.pack_start(separator, False, False, 0)
|
||||
|
||||
location_card = InactiveLocationCard(
|
||||
location, self.customer.name, self.callbacks)
|
||||
self.content_box.pack_start(location_card.widget, False, False, 0)
|
||||
|
||||
# Show all content in the box (but box itself may be hidden)
|
||||
self.content_box.show_all()
|
||||
|
||||
return card_frame
|
||||
|
||||
def _update_expand_button(self):
|
||||
"""Update the expand button arrow direction."""
|
||||
if self.expanded:
|
||||
self.expand_button.set_label("▼")
|
||||
else:
|
||||
self.expand_button.set_label("▶")
|
||||
|
||||
def _on_expand_toggle(self, button):
|
||||
"""Toggle the expanded state."""
|
||||
self.expanded = not self.expanded
|
||||
self._update_expand_button()
|
||||
self.content_box.set_visible(self.expanded)
|
||||
@@ -1,8 +1,13 @@
|
||||
from models import VPNType
|
||||
from .host_item import HostItem
|
||||
from gi.repository import Gtk
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from .host_item import HostItem
|
||||
from models import VPNType
|
||||
|
||||
|
||||
def escape_markup(text: str) -> str:
|
||||
"""Escape special characters for Pango markup."""
|
||||
return text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
class ActiveLocationCard:
|
||||
@@ -11,49 +16,78 @@ class ActiveLocationCard:
|
||||
self.customer_name = customer_name
|
||||
self.callbacks = callbacks
|
||||
self.widget = self._create_widget()
|
||||
|
||||
|
||||
def _create_widget(self):
|
||||
# Clean card layout - just a box with proper spacing
|
||||
location_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
|
||||
location_vbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
|
||||
# Location header with controls
|
||||
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
header_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
location_vbox.pack_start(header_box, False, False, 0)
|
||||
|
||||
|
||||
# Location info
|
||||
info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
header_box.pack_start(info_vbox, True, True, 0)
|
||||
|
||||
|
||||
# Location name with VPN type
|
||||
location_label = Gtk.Label()
|
||||
location_label.set_markup(f"<b>📍 {self.location.name}</b>")
|
||||
escaped_location_name = escape_markup(self.location.name)
|
||||
location_label.set_markup(f"<b>📍 {escaped_location_name}</b>")
|
||||
location_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(location_label, False, False, 0)
|
||||
|
||||
# VPN type
|
||||
|
||||
# VPN type and external address
|
||||
vpn_icons = {
|
||||
VPNType.OPENVPN: "🔒",
|
||||
VPNType.WIREGUARD: "⚡",
|
||||
VPNType.IPSEC: "🛡️"
|
||||
}
|
||||
vpn_icon = vpn_icons.get(self.location.vpn_type, "🔑")
|
||||
|
||||
|
||||
type_text = f"{vpn_icon} {self.location.vpn_type.value} VPN"
|
||||
if self.location.external_addresses:
|
||||
if len(self.location.external_addresses) == 1:
|
||||
type_text += f" • 🌐 {self.location.external_addresses[0]}"
|
||||
else:
|
||||
type_text += f" • 🌐 {len(self.location.external_addresses)} endpoints"
|
||||
|
||||
type_label = Gtk.Label()
|
||||
type_label.set_markup(f"<small>{vpn_icon} {self.location.vpn_type.value} VPN</small>")
|
||||
type_label.set_markup(f"<small>{type_text}</small>")
|
||||
type_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(type_label, False, False, 0)
|
||||
|
||||
|
||||
# External addresses and networks if available
|
||||
if self.location.external_addresses and len(self.location.external_addresses) > 1:
|
||||
# Show full list if more than one
|
||||
addresses_text = "🌐 External: " + \
|
||||
", ".join(self.location.external_addresses)
|
||||
addresses_label = Gtk.Label()
|
||||
addresses_label.set_markup(f"<small>{addresses_text}</small>")
|
||||
addresses_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(addresses_label, False, False, 0)
|
||||
|
||||
if self.location.networks:
|
||||
networks_text = "📡 Networks: " + ", ".join(self.location.networks)
|
||||
networks_label = Gtk.Label()
|
||||
networks_label.set_markup(f"<small>{networks_text}</small>")
|
||||
networks_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(networks_label, False, False, 0)
|
||||
|
||||
# Status and controls
|
||||
controls_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
controls_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
header_box.pack_end(controls_box, False, False, 0)
|
||||
|
||||
|
||||
# Status
|
||||
status_text = "● Connected" if self.location.connected else "○ Disconnected"
|
||||
status_color = "#4caf50" if self.location.connected else "#999"
|
||||
status_label = Gtk.Label()
|
||||
status_label.set_markup(f"<small><span color='{status_color}'>{status_text}</span></small>")
|
||||
status_label.set_markup(
|
||||
f"<small><span color='{status_color}'>{status_text}</span></small>")
|
||||
controls_box.pack_start(status_label, False, False, 0)
|
||||
|
||||
|
||||
# Connect/Disconnect button
|
||||
btn_text = "Disconnect" if self.location.connected else "Connect"
|
||||
connect_btn = Gtk.Button(label=btn_text)
|
||||
@@ -63,14 +97,14 @@ class ActiveLocationCard:
|
||||
connect_btn.get_style_context().add_class("suggested-action")
|
||||
connect_btn.connect("clicked", self._on_connect_clicked)
|
||||
controls_box.pack_start(connect_btn, False, False, 0)
|
||||
|
||||
|
||||
# X button to deactivate (close button style)
|
||||
close_btn = Gtk.Button(label="✕")
|
||||
close_btn.set_tooltip_text("Deactivate location")
|
||||
close_btn.get_style_context().add_class("circular")
|
||||
close_btn.connect("clicked", self._on_deactivate_clicked)
|
||||
controls_box.pack_start(close_btn, False, False, 0)
|
||||
|
||||
|
||||
# Hosts section if available
|
||||
if self.location.hosts:
|
||||
hosts_label = Gtk.Label()
|
||||
@@ -78,23 +112,26 @@ class ActiveLocationCard:
|
||||
hosts_label.set_halign(Gtk.Align.START)
|
||||
hosts_label.set_margin_top(8)
|
||||
location_vbox.pack_start(hosts_label, False, False, 0)
|
||||
|
||||
|
||||
# Hosts box with indent
|
||||
hosts_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
hosts_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
hosts_box.set_margin_start(16)
|
||||
location_vbox.pack_start(hosts_box, False, False, 0)
|
||||
|
||||
|
||||
for host in self.location.hosts:
|
||||
host_item = HostItem(host, self.callbacks['open_service'])
|
||||
host_item = HostItem(host, self.location,
|
||||
self.callbacks['open_service'])
|
||||
hosts_box.pack_start(host_item.widget, False, False, 0)
|
||||
|
||||
|
||||
return location_vbox
|
||||
|
||||
|
||||
def _on_connect_clicked(self, button):
|
||||
self.callbacks['toggle_connection'](self.location)
|
||||
|
||||
|
||||
def _on_deactivate_clicked(self, button):
|
||||
self.callbacks['deactivate_location'](self.location, self.customer_name)
|
||||
self.callbacks['deactivate_location'](
|
||||
self.location, self.customer_name)
|
||||
|
||||
|
||||
class InactiveLocationCard:
|
||||
@@ -103,54 +140,68 @@ class InactiveLocationCard:
|
||||
self.customer_name = customer_name
|
||||
self.callbacks = callbacks
|
||||
self.widget = self._create_widget()
|
||||
|
||||
|
||||
def _create_widget(self):
|
||||
# Clean horizontal layout
|
||||
location_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
|
||||
location_hbox = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
|
||||
# Location info
|
||||
info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
location_hbox.pack_start(info_vbox, True, True, 0)
|
||||
|
||||
|
||||
# Location name
|
||||
location_label = Gtk.Label()
|
||||
location_label.set_markup(f"<b>📍 {self.location.name}</b>")
|
||||
escaped_location_name = escape_markup(self.location.name)
|
||||
location_label.set_markup(f"<b>📍 {escaped_location_name}</b>")
|
||||
location_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(location_label, False, False, 0)
|
||||
|
||||
# VPN type and host count
|
||||
|
||||
# VPN type, external address and host count
|
||||
vpn_icons = {
|
||||
VPNType.OPENVPN: "🔒",
|
||||
VPNType.WIREGUARD: "⚡",
|
||||
VPNType.WIREGUARD: "⚡",
|
||||
VPNType.IPSEC: "🛡️"
|
||||
}
|
||||
vpn_icon = vpn_icons.get(self.location.vpn_type, "🔑")
|
||||
host_count = len(self.location.hosts)
|
||||
|
||||
|
||||
details_text = f"{vpn_icon} {self.location.vpn_type.value} VPN • {host_count} hosts"
|
||||
if self.location.external_addresses:
|
||||
if len(self.location.external_addresses) == 1:
|
||||
details_text += f" • 🌐 {self.location.external_addresses[0]}"
|
||||
else:
|
||||
details_text += f" • 🌐 {len(self.location.external_addresses)} endpoints"
|
||||
if self.location.networks:
|
||||
network_count = len(self.location.networks)
|
||||
details_text += f" • {network_count} network{'s' if network_count > 1 else ''}"
|
||||
|
||||
details_label = Gtk.Label()
|
||||
details_label.set_markup(f"<small>{vpn_icon} {self.location.vpn_type.value} VPN • {host_count} hosts</small>")
|
||||
details_label.set_markup(f"<small>{details_text}</small>")
|
||||
details_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(details_label, False, False, 0)
|
||||
|
||||
|
||||
# Button box for multiple buttons
|
||||
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
location_hbox.pack_end(button_box, False, False, 0)
|
||||
|
||||
|
||||
# Set as Current button
|
||||
current_btn = Gtk.Button(label="Set as Current")
|
||||
current_btn.connect("clicked", self._on_set_current_clicked)
|
||||
button_box.pack_start(current_btn, False, False, 0)
|
||||
|
||||
|
||||
# Activate button
|
||||
activate_btn = Gtk.Button(label="Set Active")
|
||||
activate_btn.get_style_context().add_class("suggested-action")
|
||||
activate_btn.connect("clicked", self._on_activate_clicked)
|
||||
button_box.pack_start(activate_btn, False, False, 0)
|
||||
|
||||
|
||||
return location_hbox
|
||||
|
||||
|
||||
def _on_activate_clicked(self, button):
|
||||
self.callbacks['set_location_active'](self.location, self.customer_name)
|
||||
|
||||
self.callbacks['set_location_active'](
|
||||
self.location, self.customer_name)
|
||||
|
||||
def _on_set_current_clicked(self, button):
|
||||
self.callbacks['set_current_location'](self.location, self.customer_name)
|
||||
self.callbacks['set_current_location'](
|
||||
self.location, self.customer_name)
|
||||
|
||||
Reference in New Issue
Block a user