Browse Source

fix: Refactor Background Jobs page

- Jobs/Worker view
- Filter by Queue Timeout and Job Status
- Toggle Auto Refresh
- Consistent theme
version-14
Faris Ansari 3 years ago
parent
commit
c7dbb61e55
9 changed files with 267 additions and 150 deletions
  1. +15
    -28
      frappe/core/page/background_jobs/background_jobs.css
  2. +54
    -47
      frappe/core/page/background_jobs/background_jobs.html
  3. +109
    -35
      frappe/core/page/background_jobs/background_jobs.js
  4. +27
    -29
      frappe/core/page/background_jobs/background_jobs.py
  5. +0
    -5
      frappe/core/page/background_jobs/background_jobs_outer.html
  6. +51
    -0
      frappe/core/page/background_jobs/background_workers.html
  7. +4
    -3
      frappe/public/js/frappe/ui/page.js
  8. +1
    -0
      frappe/public/scss/desk/page.scss
  9. +6
    -3
      frappe/utils/background_jobs.py

+ 15
- 28
frappe/core/page/background_jobs/background_jobs.css View File

@@ -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);
}

+ 54
- 47
frappe/core/page/background_jobs/background_jobs.html View File

@@ -1,51 +1,58 @@
<div class="list-jobs">
{% if jobs.length %}
<table class="table table-borderless" style="table-layout: fixed;">
<thead>
<tr>
<th style="width: 20%">{{ __("Queue / Worker") }}</th>
<th>{{ __("Job") }}</th>
<th style="width: 15%">{{ __("Created") }}</th>
</tr>
</thead>
<tbody>
{% for j in jobs %}
<tr>
<td class="worker-name">
<span class="indicator-pill no-margin {{ j.color }}"></span>
<span class="ml-2">{{ j.queue.split(".").slice(-1)[0] }}</span>
</td>
<td style="overflow: auto;">
<div>
<span class="job-name">
{{ frappe.utils.encode_tags(j.job_name) }}
</span>
</div>
{% if j.exc_info %}
{% if jobs.length %}
<table class="table table-background-jobs">
<thead>
<tr>
<th style="width: 10%">{{ __("Queue") }}</th>
<th>{{ __("Job") }}</th>
<th style="width: 10%">{{ __("Status") }}</th>
<th style="width: 15%">{{ __("Created") }}</th>
</tr>
</thead>
<tbody>
{% for j in jobs %}
<tr>
<td class="worker-name">
{{ toTitle(j.queue.split(":").slice(-1)[0]) }}
</td>
<td>
<div>
<span class="job-name">
{{ frappe.utils.encode_tags(j.job_name) }}
</span>
</div>
{% if j.exc_info %}
<details>
<summary>{{ __("Exception") }}</summary>
<div class="exc_info">
<pre>{{ frappe.utils.encode_tags(j.exc_info) }}</pre>
</div>
{% endif %}
</td>
<td class="creation">{{ j.creation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-background-jobs">
<img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Empty State">
<p class="text-muted">{{ __("No pending or current jobs for this site") }}</p>
</div>
{% endif %}
<div class="footer row">
<div class="col-md-6 text-muted text-center text-md-left">{{ __("Last refreshed") }}
{{ frappe.datetime.now_datetime(true).toLocaleString() }}</div>
<div class="col-md-6 text-center text-md-right">
<span class="indicator-pill blue" class="mr-2">{{ __("Started") }}</span>
<span class="indicator-pill orange" class="mr-2">{{ __("Queued") }}</span>
<span class="indicator-pill red" class="mr-2">{{ __("Failed") }}</span>
<span class="indicator-pill green">{{ __("Finished") }}</span>
</div>
</details>
{% endif %}
</td>
<td>
<span class="indicator-pill {{ j.color }}">
{{ toTitle(j.status) }}
</span>
</td>
<td class="creation text-muted">
{{ frappe.datetime.prettyDate(j.creation) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-background-jobs">
<img
src="/assets/frappe/images/ui-states/list-empty-state.svg"
alt="Empty State"
/>
<p class="text-muted">{{ __("No jobs found on this site") }}</p>
</div>
{% endif %}
<div class="footer">
<div class="text-muted">
{{ __("Last refreshed") }}
{{ frappe.datetime.now_datetime(true).toLocaleString() }}
</div>
</div>
</div>

+ 109
- 35
frappe/core/page/background_jobs/background_jobs.js View File

@@ -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('<div class="table-area"></div>');
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);
}
}


+ 27
- 29
frappe/core/page/background_jobs/background_jobs.py View File

@@ -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'}

+ 0
- 5
frappe/core/page/background_jobs/background_jobs_outer.html View File

@@ -1,5 +0,0 @@
<div class="frappe-card">
<div class="table-area">

</div>
</div>

+ 51
- 0
frappe/core/page/background_jobs/background_workers.html View File

@@ -0,0 +1,51 @@
{% if jobs.length %}
<table class="table table-background-jobs">
<thead>
<tr>
<th style="width: 40%">{{ __("Worker") }}</th>
<th>{{ __("Current Job") }}</th>
<th style="width: 10%">{{ __("Status") }}</th>
<th style="width: 15%">{{ __("Created") }}</th>
</tr>
</thead>
<tbody>
{% for j in jobs %}
<tr>
<td class="worker-name">
{{ j.queue }}
</td>
<td>
<div>
<span class="job-name">
{{ frappe.utils.encode_tags(j.job_name) }}
</span>
</div>
{% if j.exc_info %}
<details>
<summary>{{ __("Exception") }}</summary>
<div class="exc_info">
<pre>{{ frappe.utils.encode_tags(j.exc_info) }}</pre>
</div>
</details>
{% endif %}
</td>
<td>
<span class="indicator-pill {{ j.color }}">{{ toTitle(j.status) }}</span>
</td>
<td class="creation text-muted">{{ frappe.datetime.prettyDate(j.creation) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-background-jobs">
<img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Empty State">
<p class="text-muted">{{ __("No workers online on this site") }}</p>
</div>
{% endif %}
<div class="footer">
<div class="text-muted">
{{ __("Last refreshed") }}
{{ frappe.datetime.now_datetime(true).toLocaleString() }}
</div>
</div>

+ 4
- 3
frappe/public/js/frappe/ui/page.js View File

@@ -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) {


+ 1
- 0
frappe/public/scss/desk/page.scss View File

@@ -53,6 +53,7 @@

.custom-actions {
display: flex;
align-items: center;
}

.page-actions {


+ 6
- 3
frappe/utils/background_jobs.py View File

@@ -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'''


Loading…
Cancel
Save