Initial commit of index html and add ability to delete jobs

This commit is contained in:
Brett Williams
2022-12-07 18:24:02 -08:00
parent cc394932a1
commit fbaf2e3661
7 changed files with 252 additions and 38 deletions

View File

@@ -5,11 +5,12 @@ import logging
import os import os
import pathlib import pathlib
import shutil import shutil
import json2html
from datetime import datetime from datetime import datetime
from zipfile import ZipFile from zipfile import ZipFile
import requests import requests
from flask import Flask, request, render_template, send_file, after_this_request, Response from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from lib.render_job import RenderJob from lib.render_job import RenderJob
@@ -17,18 +18,33 @@ from lib.render_queue import RenderQueue
from utilities.render_worker import RenderWorkerFactory, string_to_status from utilities.render_worker import RenderWorkerFactory, string_to_status
logger = logging.getLogger() logger = logging.getLogger()
server = Flask(__name__) server = Flask(__name__, template_folder='../templates')
@server.route('/')
@server.route('/index')
def index():
return render_template('index.html', all_jobs=RenderQueue.job_queue, hostname=RenderQueue.host_name)
@server.route('/ui/job/<job_id>/full_details')
def job_detail(job_id):
found_job = RenderQueue.job_with_id(job_id)
if found_job:
table_html = json2html.json2html.convert(json=found_job.json())
return render_template('details.html', detail_table=table_html)
return f'Cannot find job with ID {job_id}', 400
@server.get('/api/jobs') @server.get('/api/jobs')
def jobs_json(): def jobs_json():
return [x.json_safe_copy() for x in RenderQueue.job_queue] return [x.json() for x in RenderQueue.job_queue]
@server.get('/api/jobs/<status_val>') @server.get('/api/jobs/<status_val>')
def filtered_jobs_json(status_val): def filtered_jobs_json(status_val):
state = string_to_status(status_val) state = string_to_status(status_val)
jobs = [x.json_safe_copy() for x in RenderQueue.jobs_with_status(state)] jobs = [x.json() for x in RenderQueue.jobs_with_status(state)]
if jobs: if jobs:
return jobs return jobs
else: else:
@@ -39,7 +55,7 @@ def filtered_jobs_json(status_val):
def get_job_status(job_id): def get_job_status(job_id):
found_job = RenderQueue.job_with_id(job_id) found_job = RenderQueue.job_with_id(job_id)
if found_job: if found_job:
return found_job.json_safe_copy() return found_job.json()
else: else:
return f'Cannot find job with ID {job_id}', 400 return f'Cannot find job with ID {job_id}', 400
@@ -58,17 +74,16 @@ def get_job_logs(job_id):
return f'Cannot find job with ID {job_id}', 400 return f'Cannot find job with ID {job_id}', 400
@server.get('/api/file_list/<job_id>') @server.get('/api/job/<job_id>/file_list')
def get_file_list(job_id): def get_file_list(job_id):
found_job = RenderQueue.job_with_id(job_id) found_job = RenderQueue.job_with_id(job_id)
if found_job: if found_job:
job_dir = os.path.dirname(found_job.worker.output_path) return '\n'.join(found_job.file_list())
return os.listdir(job_dir)
else: else:
return f'Cannot find job with ID {job_id}', 400 return f'Cannot find job with ID {job_id}', 400
@server.route('/api/download_all/<job_id>') @server.route('/api/job/<job_id>/download_all')
def download_all(job_id): def download_all(job_id):
zip_filename = None zip_filename = None
@@ -83,7 +98,7 @@ def download_all(job_id):
if found_job: if found_job:
output_dir = os.path.dirname(found_job.worker.output_path) output_dir = os.path.dirname(found_job.worker.output_path)
if os.path.exists(output_dir): if os.path.exists(output_dir):
zip_filename = pathlib.Path(found_job.worker.input_path).stem + '.zip' zip_filename = os.path.join('/tmp', pathlib.Path(found_job.worker.input_path).stem + '.zip')
with ZipFile(zip_filename, 'w') as zipObj: with ZipFile(zip_filename, 'w') as zipObj:
for f in os.listdir(output_dir): for f in os.listdir(output_dir):
zipObj.write(filename=os.path.join(output_dir, f), zipObj.write(filename=os.path.join(output_dir, f),
@@ -144,7 +159,7 @@ def full_status():
@server.get('/api/snapshot') @server.get('/api/snapshot')
def snapshot(): def snapshot():
server_status = RenderQueue.status() server_status = RenderQueue.status()
server_jobs = [x.json_safe_copy() for x in RenderQueue.job_queue] server_jobs = [x.json() for x in RenderQueue.job_queue]
server_data = {'status': server_status, 'jobs': server_jobs, 'timestamp': datetime.now().isoformat()} server_data = {'status': server_status, 'jobs': server_jobs, 'timestamp': datetime.now().isoformat()}
return server_data return server_data
@@ -229,7 +244,7 @@ def add_job(job_params, remove_job_dir_on_failure=False):
render_job = RenderJob(renderer, input_path, output_path, args, priority, job_owner, client, render_job = RenderJob(renderer, input_path, output_path, args, priority, job_owner, client,
notify=False, custom_id=custom_id, name=name) notify=False, custom_id=custom_id, name=name)
RenderQueue.add_to_render_queue(render_job, force_start=force_start) RenderQueue.add_to_render_queue(render_job, force_start=force_start)
return render_job.json_safe_copy() return render_job.json()
except Exception as e: except Exception as e:
err_msg = f"Error creating job: {str(e)}" err_msg = f"Error creating job: {str(e)}"
logger.exception(err_msg) logger.exception(err_msg)
@@ -272,22 +287,51 @@ def add_job(job_params, remove_job_dir_on_failure=False):
return {'error': err_msg, 'code': 400} return {'error': err_msg, 'code': 400}
@server.get('/api/cancel_job') @server.get('/api/job/<job_id>/cancel')
def cancel_job(): def cancel_job(job_id):
job_id = request.args.get('id', None) found_job = RenderQueue.job_with_id(job_id)
confirm = request.args.get('confirm', False) if not found_job:
if not job_id: return f'Cannot find job with ID {job_id}', 400
return 'job id not found', 400 elif not request.args.get('confirm', False):
elif not confirm: return 'Confirmation required to cancel job', 400
return 'confirmation required', 400
if RenderQueue.cancel_job(found_job):
return redirect(url_for('index'))
else: else:
found = [x for x in RenderQueue.job_queue if x.id == job_id] return "Unknown error", 500
if len(found) > 1:
return f'multiple jobs found for ID {job_id}', 400
elif found: @server.route('/api/job/<job_id>/delete', methods=['POST', 'GET'])
success = RenderQueue.cancel_job(found[0]) def delete_job(job_id):
return success try:
return 'job not found', 400 found_job = RenderQueue.job_with_id(job_id)
if not found_job:
return f'Cannot find job with ID {job_id}', 400
elif not request.args.get('confirm', False):
return 'Confirmation required to delete job', 400
# First, remove all render files and logs
files_to_delete = found_job.file_list()
files_to_delete.append(found_job.log_path())
for d in files_to_delete:
if os.path.exists(d):
os.remove(d)
# Check if we can remove the 'output' directory
output_dir = os.path.dirname(files_to_delete[0])
if server.config['UPLOAD_FOLDER'] in output_dir and os.path.exists(output_dir):
shutil.rmtree(output_dir)
# See if we own the input file (i.e. was it uploaded)
input_dir = os.path.dirname(found_job.worker.input_path)
if server.config['UPLOAD_FOLDER'] in input_dir and os.path.exists(input_dir):
shutil.rmtree(input_dir)
RenderQueue.delete_job(found_job)
return redirect(url_for('index'))
except Exception as e:
return "Unknown error", 500
@server.get('/api/clear_history') @server.get('/api/clear_history')
@@ -313,11 +357,6 @@ def renderer_info():
return renderer_data return renderer_data
@server.route('/')
def default():
return "Server running"
@server.route('/upload') @server.route('/upload')
def upload_file_page(): def upload_file_page():
return render_template('upload.html', render_clients=RenderQueue.render_clients, return render_template('upload.html', render_clients=RenderQueue.render_clients,

View File

@@ -1,3 +1,4 @@
import glob
import hashlib import hashlib
import json import json
import logging import logging
@@ -38,7 +39,7 @@ class RenderJob:
return hashlib.md5(open(self.worker.input_path, 'rb').read()).hexdigest() return hashlib.md5(open(self.worker.input_path, 'rb').read()).hexdigest()
return None return None
def json_safe_copy(self): def json(self):
"""Converts RenderJob into JSON-friendly dict""" """Converts RenderJob into JSON-friendly dict"""
import numbers import numbers
job_dict = None job_dict = None
@@ -97,6 +98,13 @@ class RenderJob:
def frame_count(self): def frame_count(self):
return self.worker.total_frames return self.worker.total_frames
def file_list(self):
job_dir = os.path.dirname(self.worker.output_path)
return glob.glob(os.path.join(job_dir, '*'))
def log_path(self):
return self.worker.log_path
@classmethod @classmethod
def generate_id(cls): def generate_id(cls):
return str(uuid.uuid4()).split('-')[0] return str(uuid.uuid4()).split('-')[0]

View File

@@ -126,7 +126,7 @@ class RenderQueue:
try: try:
logger.debug("Saving Render History") logger.debug("Saving Render History")
output = {'timestamp': datetime.now().isoformat(), output = {'timestamp': datetime.now().isoformat(),
'jobs': [j.json_safe_copy() for j in cls.job_queue], 'jobs': [j.json() for j in cls.job_queue],
'clients': cls.render_clients} 'clients': cls.render_clients}
output_path = json_path or JSON_FILE output_path = json_path or JSON_FILE
with open(output_path, 'w') as f: with open(output_path, 'w') as f:
@@ -167,9 +167,16 @@ class RenderQueue:
@classmethod @classmethod
def cancel_job(cls, job): def cancel_job(cls, job):
logger.info('Cancelling job ID: {}'.format(job.id)) logger.info(f'Cancelling job ID: {job.id}')
job.stop() job.stop()
return job.render_status == RenderStatus.CANCELLED return job.render_status() == RenderStatus.CANCELLED
@classmethod
def delete_job(cls, job):
logger.info(f"Deleting job ID: {job.id}")
job.stop()
cls.job_queue.remove(job)
return True
@classmethod @classmethod
def renderer_instances(cls): def renderer_instances(cls):

View File

@@ -8,3 +8,4 @@ ffmpeg-python
Werkzeug~=2.2.2 Werkzeug~=2.2.2
tkinterdnd2~=0.3.0 tkinterdnd2~=0.3.0
future~=0.18.2 future~=0.18.2
json2html~=1.3.0

View File

@@ -49,7 +49,7 @@ def start_server(background_thread=False):
server_thread.join() server_thread.join()
else: else:
server.run(host='0.0.0.0', port=RenderQueue.port, debug=config.get('flask_debug_enable', False), server.run(host='0.0.0.0', port=RenderQueue.port, debug=config.get('flask_debug_enable', False),
use_reloader=False) use_reloader=True, threaded=True)
if __name__ == '__main__': if __name__ == '__main__':

32
templates/details.html Normal file
View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zordon Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="#">
Zordon Render Server - {{hostname}}
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</nav>
<div class="container table-container">
{{detail_table|safe}}
</div>
</body>
</html>

127
templates/index.html Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zordon Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="#">
Zordon Render Server - {{hostname}}
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<!-- <div id="navbarBasicExample" class="navbar-menu">-->
<!-- <div class="navbar-start">-->
<!-- <a class="navbar-item">-->
<!-- Home-->
<!-- </a>-->
<!-- <a class="navbar-item">-->
<!-- Documentation-->
<!-- </a>-->
<!-- <div class="navbar-item has-dropdown is-hoverable">-->
<!-- <a class="navbar-link">-->
<!-- More-->
<!-- </a>-->
<!-- <div class="navbar-dropdown">-->
<!-- <a class="navbar-item">-->
<!-- About-->
<!-- </a>-->
<!-- <a class="navbar-item">-->
<!-- Jobs-->
<!-- </a>-->
<!-- <a class="navbar-item">-->
<!-- Contact-->
<!-- </a>-->
<!-- <hr class="navbar-divider">-->
<!-- <a class="navbar-item">-->
<!-- Report an issue-->
<!-- </a>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="navbar-end">-->
<!-- <div class="navbar-item">-->
<!-- <div class="buttons">-->
<!-- <a class="button is-primary">-->
<!-- <strong>Sign up</strong>-->
<!-- </a>-->
<!-- <a class="button is-light">-->
<!-- Log in-->
<!-- </a>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</nav>
<div class="table-container px-2">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<thead>
<tr>
<th>Preview</th>
<th>Name</th>
<th>Renderer</th>
<th>Priority</th>
<th>Status</th>
<th>Time Elapsed</th>
<th>Frame Count</th>
<th>Client</th>
<th>Commands</th>
</tr>
</thead>
<!-- <tfoot>-->
<!-- <tr>-->
<!-- </tr>-->
<!-- </tfoot>-->
{% for job in all_jobs %}
<tbody>
<tr>
<td>Image Here</td>
<td>{{job.name}}</td>
<td>{{job.renderer}}-{{job.worker.renderer_version}}</td>
<td>{{job.priority}}</td>
<td>{{job.render_status().value}}</td>
<td>{{job.time_elapsed()}}</td>
<td>{{job.frame_count()}}</td>
<td>{{job.client}}</td>
<td>
<div class="buttons are-small">
<button class="button is-outlined" onclick="window.location.href='/ui/job/{{job.id}}/full_details';">Details</button>
<button class="button is-outlined" onclick="window.location.href='/api/job/{{job.id}}/logs';">Logs</button>
{% if job.render_status().value in ['running', 'scheduled', 'not_started']: %}
<button class="button is-warning is-active" onclick="window.location.href='/api/job/{{job.id}}/cancel?confirm=True';">
Cancel
</button>
{% elif job.render_status().value == 'completed': %}
<button class="button is-success is-active" onclick="window.location.href='/api/job/{{job.id}}/download_all';">
Download ({{job.file_list() | length}})
</button>
{% endif %}
<button class="button is-danger is-outlined" onclick="window.location.href='/api/job/{{job.id}}/delete?confirm=True'">Delete</button>
</div>
</td>
</tr>
</tbody>
{% endfor %}
</table>
</div>
</body>
</html>