mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
New job window now lets you pick from all detected servers
This commit is contained in:
287
lib/client/dashboard_window.py
Normal file
287
lib/client/dashboard_window.py
Normal file
@@ -0,0 +1,287 @@
|
||||
import subprocess
|
||||
import logging
|
||||
import requests
|
||||
import tkinter as tk
|
||||
import threading
|
||||
import time
|
||||
import socket
|
||||
import os, sys
|
||||
from tkinter import ttk, messagebox
|
||||
from PIL import Image, ImageTk
|
||||
from lib.client.new_job_window import NewJobWindow
|
||||
from lib.server.server_proxy import RenderServerProxy
|
||||
|
||||
sys.path.append("../")
|
||||
from lib.server.zeroconf_server import ZeroconfServer
|
||||
|
||||
|
||||
|
||||
|
||||
def sort_column(tree, col, reverse=False):
|
||||
data = [(tree.set(child, col), child) for child in tree.get_children('')]
|
||||
data.sort(reverse=reverse)
|
||||
for index, (_, child) in enumerate(data):
|
||||
tree.move(child, '', index)
|
||||
|
||||
|
||||
def make_sortable(tree):
|
||||
for col in tree["columns"]:
|
||||
tree.heading(col, text=col, command=lambda c=col: sort_column(tree, c))
|
||||
|
||||
|
||||
def available_servers():
|
||||
return [socket.gethostname()]
|
||||
|
||||
|
||||
class DashboardWindow:
|
||||
|
||||
lib_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
image_path = os.path.join(lib_path, 'server', 'static', 'images', 'desktop.png')
|
||||
default_image = Image.open(image_path)
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Create a Treeview widget
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Zordon Dashboard")
|
||||
self.local_host = socket.gethostname()
|
||||
self.server_proxy = RenderServerProxy(hostname=self.local_host)
|
||||
self.job_cache = []
|
||||
|
||||
# Setup zeroconf
|
||||
self.zeroconf = ZeroconfServer("_zordon._tcp.local.", socket.gethostname(), 8080)
|
||||
self.zeroconf.start(listen_only=True)
|
||||
|
||||
# Setup photo preview
|
||||
photo_pad = tk.Frame(self.root, background="gray")
|
||||
photo_pad.pack(fill=tk.BOTH, pady=5, padx=5)
|
||||
self.photo_label = tk.Label(photo_pad, height=500)
|
||||
self.photo_label.pack(fill=tk.BOTH, expand=True)
|
||||
self.set_image(self.default_image)
|
||||
|
||||
server_frame = tk.LabelFrame(self.root, text="Server")
|
||||
server_frame.pack(fill=tk.BOTH, pady=5, padx=5, expand=True)
|
||||
|
||||
# Create server tree
|
||||
self.server_tree = ttk.Treeview(server_frame, show="headings")
|
||||
self.server_tree.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
|
||||
self.server_tree["columns"] = ("Server")
|
||||
self.server_tree.column("Server", width=50)
|
||||
self.server_tree.bind("<<TreeviewSelect>>", self.server_picked)
|
||||
self.server_tree.column("Server", width=200)
|
||||
make_sortable(self.server_tree)
|
||||
|
||||
# Create separator
|
||||
separator = ttk.Separator(server_frame, orient=tk.VERTICAL)
|
||||
separator.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.Y)
|
||||
|
||||
# Setup the Tree
|
||||
self.job_tree = ttk.Treeview(server_frame, show="headings", height=10)
|
||||
self.job_tree.tag_configure('running', background='lawn green', font=('', 0, 'bold'))
|
||||
self.job_tree.bind("<<TreeviewSelect>>", self.on_row_select)
|
||||
self.job_tree["columns"] = ("id", "Name", "Renderer", "Priority", "Status", "Time Elapsed", "Frames", "")
|
||||
|
||||
# Format the columns
|
||||
self.job_tree.column("id", width=0, stretch=False)
|
||||
self.job_tree.column("Name", width=300)
|
||||
self.job_tree.column("Renderer", width=100, stretch=False)
|
||||
self.job_tree.column("Priority", width=50, stretch=False)
|
||||
self.job_tree.column("Status", width=100, stretch=False)
|
||||
self.job_tree.column("Time Elapsed", width=100, stretch=False)
|
||||
self.job_tree.column("Frames", width=50, stretch=False)
|
||||
|
||||
# Create the column headings
|
||||
for name in self.job_tree['columns']:
|
||||
self.job_tree.heading(name, text=name)
|
||||
|
||||
# Pack the Treeview widget
|
||||
self.job_tree.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
button_frame = tk.Frame(server_frame)
|
||||
button_frame.pack(pady=5, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create buttons
|
||||
logs_button = tk.Button(button_frame, text="Logs", command=self.open_logs)
|
||||
finder_button = tk.Button(button_frame, text="Reveal in Finder", command=self.reveal_in_finder)
|
||||
self.stop_button = tk.Button(button_frame, text="Stop", command=self.stop_job)
|
||||
delete_button = tk.Button(button_frame, text="Delete", command=self.delete_job)
|
||||
add_job_button = tk.Button(button_frame, text="Add Job", command=self.show_new_job_window)
|
||||
|
||||
self.stop_button.config(state='disabled')
|
||||
|
||||
# Pack the buttons in the frame
|
||||
self.stop_button.pack(side=tk.LEFT)
|
||||
delete_button.pack(side=tk.LEFT)
|
||||
finder_button.pack(side=tk.LEFT)
|
||||
logs_button.pack(side=tk.LEFT)
|
||||
add_job_button.pack(side=tk.RIGHT)
|
||||
|
||||
# Start the Tkinter event loop
|
||||
self.root.geometry("500x600+300+300")
|
||||
self.root.maxsize(width=2000, height=1200)
|
||||
self.root.minsize(width=900, height=800)
|
||||
make_sortable(self.job_tree)
|
||||
|
||||
# update jobs
|
||||
self.update_jobs()
|
||||
try:
|
||||
selected_job = self.job_tree.get_children()[0]
|
||||
self.job_tree.selection_set(selected_job)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# update servers
|
||||
self.update_servers()
|
||||
try:
|
||||
selected_server = self.server_tree.get_children()[0]
|
||||
self.server_tree.selection_set(selected_server)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# start background update
|
||||
x = threading.Thread(target=self.__background_update)
|
||||
x.daemon = True
|
||||
x.start()
|
||||
|
||||
def server_picked(self, event):
|
||||
try:
|
||||
new_hostname = self.server_tree.selection()[0]
|
||||
self.server_proxy.hostname = new_hostname
|
||||
except IndexError:
|
||||
pass
|
||||
self.job_cache.clear()
|
||||
self.update_jobs(clear_table=True)
|
||||
|
||||
def selected_job_id(self):
|
||||
try:
|
||||
selected_item = self.job_tree.selection()[0] # Get the selected item
|
||||
row_data = self.job_tree.item(selected_item) # Get the text of the selected item
|
||||
job_id = row_data['values'][0]
|
||||
return job_id
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def stop_job(self):
|
||||
job_id = self.selected_job_id()
|
||||
self.server_proxy.request_data(f'job/{job_id}/cancel?confirm=true')
|
||||
|
||||
def delete_job(self):
|
||||
job_id = self.selected_job_id()
|
||||
job = next((d for d in self.job_cache if d.get('id') == job_id), None)
|
||||
display_name = job['name'] or os.path.basename(job['input_path'])
|
||||
result = messagebox.askyesno("Confirmation", f"Are you sure you want to delete the job:\n{display_name}?")
|
||||
if result:
|
||||
self.server_proxy.request_data(f'job/{job_id}/delete?confirm=true')
|
||||
self.job_cache.clear()
|
||||
self.update_jobs(clear_table=True)
|
||||
|
||||
def set_image(self, image):
|
||||
thumb_image = ImageTk.PhotoImage(image)
|
||||
if thumb_image:
|
||||
self.photo_label.configure(image=thumb_image)
|
||||
self.photo_label.image = thumb_image
|
||||
|
||||
def on_row_select(self, event):
|
||||
job_id = self.selected_job_id()
|
||||
if job_id:
|
||||
# update thumb
|
||||
response = self.server_proxy.request(f'job/{job_id}/thumbnail?size=big&video_ok=false')
|
||||
if response.ok:
|
||||
try:
|
||||
import io
|
||||
image_data = response.content
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
self.set_image(image)
|
||||
except Exception as e:
|
||||
print(f"error getting image: {e}")
|
||||
else:
|
||||
self.set_image(self.default_image)
|
||||
|
||||
# update button status
|
||||
job = next((d for d in self.job_cache if d.get('id') == job_id), None)
|
||||
button_state = 'normal' if job and job['status'] == 'running' else 'disabled'
|
||||
self.stop_button.config(state=button_state)
|
||||
|
||||
def reveal_in_finder(self):
|
||||
job = next((d for d in self.job_cache if d.get('id') == self.selected_job_id()), None)
|
||||
output_dir = os.path.dirname(job['output_path'])
|
||||
subprocess.run(['open', output_dir])
|
||||
|
||||
def open_logs(self):
|
||||
url = f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/job/{self.selected_job_id()}/logs'
|
||||
subprocess.run(['open', url])
|
||||
|
||||
def mainloop(self):
|
||||
self.root.mainloop()
|
||||
|
||||
def __background_update(self):
|
||||
while True:
|
||||
self.update_jobs()
|
||||
self.update_servers()
|
||||
time.sleep(3)
|
||||
|
||||
def update_servers(self):
|
||||
servers = self.zeroconf.found_clients()
|
||||
if len(servers) < len(self.server_tree.get_children()):
|
||||
self.server_tree.delete(*self.server_tree.get_children())
|
||||
for hostname in servers:
|
||||
if hostname not in self.server_tree.get_children():
|
||||
self.server_tree.insert("", tk.END, iid=hostname, values=(hostname,))
|
||||
|
||||
def update_jobs(self, clear_table=False):
|
||||
|
||||
def update_jobs_inner():
|
||||
def update_row(tree, id, new_values, tags=None):
|
||||
for item in tree.get_children():
|
||||
values = tree.item(item, "values")
|
||||
if values[0] == id:
|
||||
tree.item(item, values=new_values, tags=tags)
|
||||
break
|
||||
|
||||
job_fetch = self.server_proxy.get_jobs()
|
||||
if job_fetch is not None:
|
||||
if len(job_fetch) < len(self.job_cache) or len(job_fetch) == 0:
|
||||
self.job_tree.delete(*self.job_tree.get_children())
|
||||
self.job_cache = job_fetch # update the cache only if its good data
|
||||
for job in self.job_cache:
|
||||
display_status = job['status'] if job['status'] != 'running' else \
|
||||
('%.0f%%' % (job['percent_complete'] * 100)) # if running, show percent, otherwise just show status
|
||||
tags = (job['status'],)
|
||||
values = (job['id'],
|
||||
job['name'] or os.path.basename(job['input_path']),
|
||||
job['renderer'] + "-" + job['renderer_version'],
|
||||
job['priority'],
|
||||
display_status,
|
||||
job['time_elapsed'],
|
||||
job['total_frames'])
|
||||
try:
|
||||
if self.job_tree.exists(job['id']):
|
||||
update_row(self.job_tree, job['id'], new_values=values, tags=tags)
|
||||
else:
|
||||
self.job_tree.insert("", tk.END, iid=job['id'], values=values, tags=tags)
|
||||
except tk.TclError:
|
||||
pass
|
||||
|
||||
if clear_table:
|
||||
self.job_tree.delete(*self.job_tree.get_children())
|
||||
|
||||
x = threading.Thread(target=update_jobs_inner)
|
||||
x.daemon = True
|
||||
x.start()
|
||||
|
||||
def show_new_job_window(self):
|
||||
new_window = tk.Toplevel(self.root)
|
||||
new_window.title("New Window")
|
||||
new_window.geometry("500x600+300+300")
|
||||
new_window.resizable(False, height=True)
|
||||
x = NewJobWindow(parent=new_window, clients=list(self.server_tree.get_children()))
|
||||
x.pack()
|
||||
|
||||
|
||||
def start_client():
|
||||
x = DashboardWindow()
|
||||
x.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start_client()
|
||||
Reference in New Issue
Block a user