mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 08:48:13 +00:00
Initial commit of index html and add ability to delete jobs
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
32
templates/details.html
Normal 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
127
templates/index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user