From c7dbb61e55a89253031c7a4a42812f9e3cee8ce2 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 31 Mar 2022 16:35:54 +0530 Subject: [PATCH] fix: Refactor Background Jobs page - Jobs/Worker view - Filter by Queue Timeout and Job Status - Toggle Auto Refresh - Consistent theme --- .../page/background_jobs/background_jobs.css | 43 ++---- .../page/background_jobs/background_jobs.html | 101 ++++++------ .../page/background_jobs/background_jobs.js | 144 +++++++++++++----- .../page/background_jobs/background_jobs.py | 56 ++++--- .../background_jobs_outer.html | 5 - .../background_jobs/background_workers.html | 51 +++++++ frappe/public/js/frappe/ui/page.js | 7 +- frappe/public/scss/desk/page.scss | 1 + frappe/utils/background_jobs.py | 9 +- 9 files changed, 267 insertions(+), 150 deletions(-) delete mode 100644 frappe/core/page/background_jobs/background_jobs_outer.html create mode 100644 frappe/core/page/background_jobs/background_workers.html diff --git a/frappe/core/page/background_jobs/background_jobs.css b/frappe/core/page/background_jobs/background_jobs.css index 0c77522cb3..7716519113 100644 --- a/frappe/core/page/background_jobs/background_jobs.css +++ b/frappe/core/page/background_jobs/background_jobs.css @@ -1,43 +1,32 @@ -.list-jobs { - font-size: var(--text-base); -} -.table { +.table-background-jobs { margin-bottom: 0px; margin-top: 0px; + font-size: var(--text-md); + table-layout: fixed; } -thead { - background-color: var(--control-bg); - border-radius: var(--border-radius-sm); +.table-background-jobs th { + font-weight: normal; + color: var(--text-muted); } -thead > tr { - border-radius: var(--border-radius-sm); +.table-background-jobs td { + color: var(--text-light); } -thead > tr > th:first-child { - border-radius: var(--border-radius-sm) 0 0 var(--border-radius-sm); -} -thead > tr > th:last-child { - border-radius: 0 var(--border-radius-sm) var(--border-radius-sm) 0; +.table-background-jobs th, .table-background-jobs td { + padding: var(--padding-sm) var(--padding-md); } -.worker-name { - display: flex; - align-items: center; +.table-background-jobs tbody tr:hover { + background-color: var(--highlight-color); } .job-name { font-size: var(--text-md); - font-family: "Courier New", Courier, monospace; - /* background-color: var(--control-bg); */ - /* padding: var(--padding-xs) var(--padding-sm); */ - /* border-radius: var(--border-radius-md); */ -} - -.background-job-row:hover { - background-color: var(--bg-color); + font-family: var(--font-family-monospace); + word-break: break-word; } .no-background-jobs { @@ -54,7 +43,5 @@ thead > tr > th:last-child { } .footer { - align-items: flex-end; - margin-top: var(--margin-md); - font-size: var(--text-base); + padding: var(--padding-md); } diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index 1b00ec3106..e0c1a8f633 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -1,51 +1,58 @@ -
- {% if jobs.length %} - - - - - - - - - - {% for j in jobs %} - - - + + + + {% endfor %} + +
{{ __("Queue / Worker") }}{{ __("Job") }}{{ __("Created") }}
- - {{ j.queue.split(".").slice(-1)[0] }} - -
- - {{ frappe.utils.encode_tags(j.job_name) }} - -
- {% if j.exc_info %} +{% if jobs.length %} + + + + + + + + + + + {% for j in jobs %} + + + - - - {% endfor %} - -
{{ __("Queue") }}{{ __("Job") }}{{ __("Status") }}{{ __("Created") }}
+ {{ toTitle(j.queue.split(":").slice(-1)[0]) }} + +
+ + {{ frappe.utils.encode_tags(j.job_name) }} + +
+ {% if j.exc_info %} +
+ {{ __("Exception") }}
{{ frappe.utils.encode_tags(j.exc_info) }}
- {% endif %} -
{{ j.creation }}
- {% else %} -
- Empty State -

{{ __("No pending or current jobs for this site") }}

-
- {% endif %} -
+ + {{ toTitle(j.status) }} + + + {{ frappe.datetime.prettyDate(j.creation) }} +
+{% else %} +
+ Empty State +

{{ __("No jobs found on this site") }}

+
+{% endif %} + \ No newline at end of file +
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js index 0b4d6792dc..7334bfd5dd 100644 --- a/frappe/core/page/background_jobs/background_jobs.js +++ b/frappe/core/page/background_jobs/background_jobs.js @@ -1,7 +1,7 @@ -frappe.pages["background_jobs"].on_page_load = (wrapper) => { +frappe.pages["background_jobs"].on_page_load = wrapper => { const background_job = new BackgroundJobs(wrapper); - $(wrapper).bind('show', () => { + $(wrapper).bind("show", () => { background_job.show(); }); @@ -12,61 +12,135 @@ class BackgroundJobs { constructor(wrapper) { this.page = frappe.ui.make_app_page({ parent: wrapper, - title: __('Background Jobs'), + title: __("Background Jobs"), single_column: true }); - this.called = false; - this.show_failed = false; + this.page.add_inner_button(__("Remove Failed Jobs"), () => { + frappe.confirm( + __("Are you sure you want to remove all failed jobs?"), + () => { + frappe + .call( + "frappe.core.page.background_jobs.background_jobs.remove_failed_jobs" + ) + .then(() => this.refresh_jobs()); + } + ); + }); - this.show_failed_button = this.page.add_inner_button(__("Show Failed Jobs"), () => { - this.show_failed = !this.show_failed; - if (this.show_failed_button) { - this.show_failed_button.text( - this.show_failed ? __("Hide Failed Jobs") : __("Show Failed Jobs") - ); + this.page.main.addClass("frappe-card"); + this.page.body.append('
'); + this.$content = $(this.page.body).find(".table-area"); + + this.make_filters(); + this.refresh_jobs = frappe.utils.throttle( + this.refresh_jobs.bind(this), + 1000 + ); + } + + make_filters() { + this.view = this.page.add_field({ + label: __("View"), + fieldname: "view", + fieldtype: "Select", + options: ["Jobs", "Workers"], + default: "Jobs", + change: () => { + this.queue_timeout.toggle(this.view.get_value() === "Jobs"); + this.job_status.toggle(this.view.get_value() === "Jobs"); } }); - - // add a "Remove Failed Jobs button" - this.remove_failed_button = this.page.add_inner_button(__("Remove Failed Jobs"), () => { - frappe.call({ - method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs', - callback: () => { + this.queue_timeout = this.page.add_field({ + label: __("Queue"), + fieldname: "queue_timeout", + fieldtype: "Select", + options: [ + { label: "All Queues", value: "all" }, + { label: "Default", value: "default" }, + { label: "Short", value: "short" }, + { label: "Long", value: "long" } + ], + default: "all" + }); + this.job_status = this.page.add_field({ + label: __("Job Status"), + fieldname: "job_status", + fieldtype: "Select", + options: [ + { label: "All Jobs", value: "all" }, + { label: "Queued", value: "queued" }, + { label: "Deferred", value: "deferred" }, + { label: "Started", value: "started" }, + { label: "Finished", value: "finished" }, + { label: "Failed", value: "failed" } + ], + default: "all" + }); + this.auto_refresh = this.page.add_field({ + label: __("Auto Refresh"), + fieldname: "auto_refresh", + fieldtype: "Check", + default: 1, + change: () => { + if (this.auto_refresh.get_value()) { this.refresh_jobs(); } - }); + } }); - - $(frappe.render_template('background_jobs_outer')).appendTo(this.page.body); - this.content = $(this.page.body).find('.table-area'); } show() { this.refresh_jobs(); + this.update_scheduler_status(); + } + + update_scheduler_status() { frappe.call({ - method: 'frappe.core.page.background_jobs.background_jobs.get_scheduler_status', - callback: res => { - this.page.set_indicator(...res.message); + method: + "frappe.core.page.background_jobs.background_jobs.get_scheduler_status", + callback: r => { + let { status } = r.message; + if (status === "active") { + this.page.set_indicator(__("Scheduler: Active"), "green"); + } else { + this.page.set_indicator(__("Scheduler: Inactive"), "red"); + } } }); } refresh_jobs() { - if (this.called) return; - this.called = true; + let view = this.view.get_value(); + let args; + let { queue_timeout, job_status } = this.page.get_form_values(); + if (view === "Jobs") { + args = { view, queue_timeout, job_status }; + } else { + args = { view }; + } + this.page.add_inner_message(__("Refreshing...")); frappe.call({ - method: 'frappe.core.page.background_jobs.background_jobs.get_info', - args: { - show_failed: this.show_failed - }, - callback: (res) => { - this.called = false; - this.page.body.find('.list-jobs').remove(); - $(frappe.render_template('background_jobs', { jobs: res.message || [] })).appendTo(this.content); + method: "frappe.core.page.background_jobs.background_jobs.get_info", + args, + callback: res => { + this.page.add_inner_message(""); + + let template = + view === "Jobs" ? "background_jobs" : "background_workers"; + this.$content.html( + frappe.render_template(template, { + jobs: res.message || [] + }) + ); - if (frappe.get_route()[0] === 'background_jobs') { + let auto_refresh = this.auto_refresh.get_value(); + if ( + frappe.get_route()[0] === "background_jobs" && + auto_refresh + ) { setTimeout(() => this.refresh_jobs(), 2000); } } diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 4d9deca526..960444c349 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -8,8 +8,8 @@ from rq import Worker import frappe from frappe import _ -from frappe.utils import convert_utc_to_user_timezone, format_datetime -from frappe.utils.background_jobs import get_redis_conn, get_queues +from frappe.utils import convert_utc_to_user_timezone +from frappe.utils.background_jobs import get_queues, get_workers from frappe.utils.scheduler import is_scheduler_inactive if TYPE_CHECKING: @@ -24,16 +24,15 @@ JOB_COLORS = { @frappe.whitelist() -def get_info(show_failed=False) -> List[Dict]: - if isinstance(show_failed, str): - show_failed = json.loads(show_failed) - - conn = get_redis_conn() - queues = get_queues() - workers = Worker.all(conn) +def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]: jobs = [] def add_job(job: 'Job', name: str) -> None: + if job_status != "all" and job.get_status() != job_status: + return + if queue_timeout != "all" and not name.endswith(f':{queue_timeout}'): + return + if job.kwargs.get('site') == frappe.local.site: job_info = { 'job_name': job.kwargs.get('kwargs', {}).get('playbook_method') @@ -41,7 +40,7 @@ def get_info(show_failed=False) -> List[Dict]: or str(job.kwargs.get('job_name')), 'status': job.get_status(), 'queue': name, - 'creation': format_datetime(convert_utc_to_user_timezone(job.created_at)), + 'creation': convert_utc_to_user_timezone(job.created_at), 'color': JOB_COLORS[job.get_status()] } @@ -50,32 +49,31 @@ def get_info(show_failed=False) -> List[Dict]: jobs.append(job_info) - # show worker jobs - for worker in workers: - job = worker.get_current_job() - if job: - add_job(job, worker.name) - - for queue in queues: - # show active queued jobs - if queue.name != 'failed': + if view == 'Jobs': + queues = get_queues() + for queue in queues: for job in queue.jobs: add_job(job, queue.name) - # show failed jobs, if requested - if show_failed: - fail_registry = queue.failed_job_registry - for job_id in fail_registry.get_job_ids(): - job = queue.fetch_job(job_id) - if job: - add_job(job, queue.name) + elif view == 'Workers': + workers = get_workers() + for worker in workers: + current_job = worker.get_current_job() + if current_job and current_job.kwargs.get('site') == frappe.local.site: + add_job(current_job, job.origin) + else: + jobs.append({ + 'queue': worker.name, + 'job_name': 'idle', + 'status': '', + 'creation': '' + }) return jobs @frappe.whitelist() def remove_failed_jobs(): - conn = get_redis_conn() queues = get_queues() for queue in queues: fail_registry = queue.failed_job_registry @@ -87,5 +85,5 @@ def remove_failed_jobs(): @frappe.whitelist() def get_scheduler_status(): if is_scheduler_inactive(): - return [_("Inactive"), "red"] - return [_("Active"), "green"] + return {'status': 'inactive'} + return {'status': 'active'} diff --git a/frappe/core/page/background_jobs/background_jobs_outer.html b/frappe/core/page/background_jobs/background_jobs_outer.html deleted file mode 100644 index 4ca3a32906..0000000000 --- a/frappe/core/page/background_jobs/background_jobs_outer.html +++ /dev/null @@ -1,5 +0,0 @@ -
-
- -
-
\ No newline at end of file diff --git a/frappe/core/page/background_jobs/background_workers.html b/frappe/core/page/background_jobs/background_workers.html new file mode 100644 index 0000000000..1647cea4b4 --- /dev/null +++ b/frappe/core/page/background_jobs/background_workers.html @@ -0,0 +1,51 @@ +{% if jobs.length %} + + + + + + + + + + + {% for j in jobs %} + + + + + + + {% endfor %} + +
{{ __("Worker") }}{{ __("Current Job") }}{{ __("Status") }}{{ __("Created") }}
+ {{ j.queue }} + +
+ + {{ frappe.utils.encode_tags(j.job_name) }} + +
+ {% if j.exc_info %} +
+ {{ __("Exception") }} +
+
{{ frappe.utils.encode_tags(j.exc_info) }}
+
+
+ {% endif %} +
+ {{ toTitle(j.status) }} + {{ frappe.datetime.prettyDate(j.creation) }}
+{% else %} +
+ Empty State +

{{ __("No workers online on this site") }}

+
+{% endif %} + \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 91a2390cdb..34189ed025 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -846,9 +846,10 @@ frappe.ui.Page = class Page { } get_form_values() { var values = {}; - this.page_form.fields_dict.forEach(function(field, key) { - values[key] = field.get_value(); - }); + for (let fieldname in this.fields_dict) { + let field = this.fields_dict[fieldname]; + values[fieldname] = field.get_value(); + } return values; } add_view(name, html) { diff --git a/frappe/public/scss/desk/page.scss b/frappe/public/scss/desk/page.scss index f0a9152cfb..00b537b919 100644 --- a/frappe/public/scss/desk/page.scss +++ b/frappe/public/scss/desk/page.scss @@ -53,6 +53,7 @@ .custom-actions { display: flex; + align-items: center; } .page-actions { diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 58029dbc5f..ac58577c2e 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -220,9 +220,12 @@ def get_queue_list(queue_list=None, build_queue_name=False): queue_list = default_queue_list return [generate_qname(qtype) for qtype in queue_list] if build_queue_name else queue_list -def get_workers(queue): - '''Returns a list of Worker objects tied to a queue object''' - return Worker.all(queue=queue) +def get_workers(queue=None): + '''Returns a list of Worker objects tied to a queue object if queue is passed, else returns a list of all workers''' + if queue: + return Worker.all(queue=queue) + else: + return Worker.all(get_redis_conn()) def get_running_jobs_in_queue(queue): '''Returns a list of Jobs objects that are tied to a queue object and are currently running'''