Vue rewrite of admin jobs page

This commit is contained in:
Sebastian Kapust
2019-04-20 17:49:01 +02:00
parent eaff93c6b5
commit cd37a846a2
6 changed files with 437 additions and 255 deletions
@@ -0,0 +1,371 @@
<template>
<div>
<h2 class="mb-3">
<span id="jobs-title">Jobs (vue)</span>
</h2>
<Alert :message="message" :variant="status" />
<b-alert variant="info" show>
<p>
Unfinished and recently finished jobs are displayed on this page. The 'cutoff' input box will do two
things -- it will limit the display of unfinished jobs to only those jobs that have not had their job
state updated recently, and it will limit the recently finished jobs list to only displaying jobs that
have finished since the cutoff.
</p>
If any jobs are displayed, you may choose to stop them. Your stop message will be displayed to the user as:
"This job was stopped by an administrator: <strong>&lt;YOUR MESSAGE&gt;</strong> For more information or
help, report this error".
</b-alert>
<Alert v-if="loading" message="Waiting for data" variant="info" />
<div v-else>
<b-container class="mb-3">
<b-row>
<b-col md="6">
<b-form @submit.prevent="onRefresh">
<b-form-group
id="cutoff"
label="Update Jobs"
label-for="cutoff-seconds"
description="Cutoff in seconds"
>
<b-input-group>
<b-form-input id="cutoff" type="number" placeholder="180" v-model="cutoff">
</b-form-input>
<b-input-group-append>
<b-btn type="submit">Refresh</b-btn>
</b-input-group-append>
</b-input-group>
</b-form-group>
</b-form>
</b-col>
<b-col>
<b-form @submit.prevent="onUpdateJobLock">
<b-form-group label="Administrative Job Lock" label-for="prevent-job-dispatching">
<template slot="description">
Job dispatching is currently
<strong>{{ jobLockDisplay ? "locked" : "unlocked" }}</strong
>.
</template>
<b-container>
<b-row>
<b-col md="auto">
<b-form-checkbox id="prevent-job-dispatching" v-model="jobLock">
<strong>Prevent jobs from dispatching</strong>
</b-form-checkbox>
</b-col>
<b-col>
<b-btn type="submit">Update</b-btn>
</b-col>
</b-row>
</b-container>
</b-form-group>
</b-form>
</b-col>
</b-row>
<b-row>
<b-col md="6">
<b-form-group
label="Filter Jobs"
label-for="filter-regex"
description="Search for strings or regular expressions"
>
<b-input-group id="filter-regex">
<b-form-input
v-model="filter"
placeholder="Type to Search"
@keyup.esc.native="filter = ''"
/>
<b-input-group-append>
<b-btn :disabled="!filter" @click="filter = ''">Clear (esc)</b-btn>
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-button :pressed.sync="showCommandLine" variant="outline-secondary">
{{ showCommandLine ? "Hide" : "Show" }} Command Line
</b-button>
</b-col>
<b-col>
<b-card
v-if="jobsItemsComputed.length"
header="Stop Selected Jobs"
header-bg-variant="info"
header-text-variant="white"
border-variant="info"
>
<b-form @submit.prevent="onStopJobs">
<b-form-group label-for="stop-message" description="will be displayed to the user">
<b-input-group>
<b-form-input
id="stop-message"
v-model="stopMessage"
placeholder="Stop message"
required
>
</b-form-input>
<b-input-group-append>
<b-btn type="submit">Submit</b-btn>
</b-input-group-append>
</b-input-group>
</b-form-group>
</b-form>
</b-card>
</b-col>
</b-row>
</b-container>
<b-alert v-if="!jobsItemsComputed.length" variant="secondary" show>
There are no unfinished jobs to show with current cutoff time.
</b-alert>
<b-table
v-else
:fields="jobsFieldsComputed"
:items="jobsItemsComputed"
v-model="jobsItemsModel"
:filter="filter"
hover
responsive
striped
caption-top
@row-clicked="showRowDetails"
:busy="busy"
>
<template slot="table-caption">
Unfinished Jobs: These jobs are unfinished and have had their state updated in the previous
{{ cutoffDisplay }} seconds.
</template>
<!-- Enable cell formatting for the command line column -->
<span slot="html" slot-scope="data" v-html="data.value" />
<template slot="HEAD_selected" slot-scope="data">
<b-form-checkbox
v-model="allSelected"
:indeterminate="indeterminate"
@change="toggleAll"
></b-form-checkbox>
</template>
<template slot="selected" slot-scope="data">
<b-form-checkbox
v-model="selectedStopJobIds"
:checked="allSelected"
:key="data.index"
:value="data.item['job_info']['id']"
></b-form-checkbox>
</template>
<template slot="job_info" slot-scope="data">
<b-link :href="data.value['info_url']" target="galaxy_main">
{{ data.value["id"] }}
</b-link>
</template>
<template slot="row-details" slot-scope="row">
<b-card>
<h5>Command Line</h5>
<pre class="code"><code class="break-word">{{ row.item.command_line }}</code></pre>
</b-card>
</template>
</b-table>
<b-alert v-if="!recentJobsItemsComputed.length" variant="secondary" show>
There are no recently finished jobs to show with current cutoff time.
</b-alert>
<b-table
v-else
:fields="recentJobsFieldsComputed"
:items="recentJobsItemsComputed"
:filter="filter"
hover
responsive
striped
caption-top
@row-clicked="showRowDetails"
:busy="busy"
>
<template slot="table-caption">
Recent Jobs: These jobs have completed in the previous {{ cutoffDisplay }} seconds.
</template>
<!-- Enable cell formatting for the command line column -->
<span slot="html" slot-scope="data" v-html="data.value" />
<template slot="job_info" slot-scope="data">
<b-link :href="data.value['info_url']" target="galaxy_main">
{{ data.value["id"] }}
</b-link>
</template>
<template slot="row-details" slot-scope="row">
<b-card>
<h5>Command Line</h5>
<pre class="code"><code class="break-word">{{ row.item.command_line }}</code></pre>
</b-card>
</template>
</b-table>
</div>
</div>
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import axios from "axios";
import Alert from "components/Alert.vue";
export default {
components: {
Alert
},
data() {
return {
jobsItems: [],
jobsItemsModel: [],
recentJobsItems: [],
jobsFields: [
{ key: "job_info", label: "Job ID", sortable: true },
{ key: "user" },
{ key: "tool_id", label: "Tool", tdClass: ["break-word"] },
{ key: "state" },
{ key: "input_dataset", label: "Inputs" },
{ key: "job_runner_name", label: "Job Runner" },
{ key: "job_runner_external_id", label: "PID/Cluster ID", sortable: true }
],
showCommandLine: false,
cutoff: 180,
cutoffDisplay: 180,
jobLock: false,
jobLockDisplay: false,
selectedStopJobIds: [],
allSelected: false,
indeterminate: false,
stopMessage: "",
filter: "",
message: "",
status: "",
loading: true,
busy: true
};
},
watch: {
selectedStopJobIds(newVal) {
if (newVal.length === 0) {
this.indeterminate = false;
this.allSelected = false;
} else if (newVal.length === this.jobsItemsModel.length) {
this.indeterminate = false;
this.allSelected = true;
} else {
this.indeterminate = true;
this.allSelected = false;
}
}
},
methods: {
update() {
this.busy = true;
let params = [];
params.push(`cutoff=${this.cutoff}`);
params.push(`stop_msg=${this.stopMessage}`);
params.push(`stop=${this.selectedStopJobIds.join()}`);
params = params.join("&");
axios
.get(`${getAppRoot()}admin/jobs_list?${params}`)
.then(response => {
this.jobsItems = response.data.jobs;
this.recentJobsItems = response.data.recent_jobs;
this.cutoffDisplay = response.data.cutoff;
this.message = response.data.message;
this.status = response.data.status;
this.jobLockDisplay = response.data.job_lock;
this.loading = false;
this.busy = false;
})
.catch(error => {
console.error(error);
});
},
onRefresh() {
this.update();
},
onUpdateJobLock() {
axios
.get(`${getAppRoot()}admin/jobs_control?job_lock=${this.jobLock}`)
.then(response => {
this.jobLockDisplay = response.data.job_lock;
})
.catch(error => {
console.error(error);
});
},
onStopJobs() {
this.update();
this.selectedStopJobIds = [];
this.stopMessage = "";
},
showRowDetails(row, index, e) {
if (e.target.nodeName != "A") {
row._showDetails = !row._showDetails;
}
},
translateState(state) {
const translateDict = {
ok: "success",
error: "danger",
new: "primary",
queued: "secondary",
running: "info",
upload: "dark"
};
return translateDict[state] || "primary";
},
computeItems(items) {
return items.map(job => {
return {
...job,
_showDetails: false,
_cellVariants: { state: this.translateState(job.state) }
};
});
},
computeFields(fields) {
let f = Array.from(fields).slice(0);
if (this.showCommandLine) {
f.splice(6, 0, {
key: "command_line",
tdClass: ["code", "break-word"]
});
}
return f;
},
toggleAll(checked) {
this.selectedStopJobIds = checked
? this.jobsItemsModel.reduce((acc, j) => [...acc, j["job_info"]["id"]], [])
: [];
}
},
computed: {
jobsItemsComputed() {
return this.computeItems(this.jobsItems);
},
recentJobsItemsComputed() {
return this.computeItems(this.recentJobsItems);
},
jobsFieldsComputed() {
let f = this.jobsFields.slice(0);
f.splice(0, 0, { key: "selected", label: "", variant: "info" });
f.splice(2, 0, { key: "update_time", label: "Last Update", sortable: true });
return this.computeFields(f);
},
recentJobsFieldsComputed() {
let f = this.jobsFields.slice(0);
f.splice(2, 0, { key: "update_time", label: "Finished", sortable: true });
return this.computeFields(f);
}
},
created() {
this.update();
}
};
</script>
<style>
/* Can not be scoped because of command line tdClass */
.code {
background: black;
color: white;
padding: 1em;
}
.break-word {
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -7,6 +7,7 @@ import QueryStringParsing from "utils/query-string-parsing";
import Router from "layout/router";
import DataTables from "components/admin/DataTables.vue";
import DataTypes from "components/admin/DataTypes.vue";
import Jobs from "components/admin/Jobs.vue";
import DataManagerView from "components/admin/DataManager/DataManagerView.vue";
import DataManagerRouter from "components/admin/DataManager/DataManagerRouter.vue";
import Register from "components/login/Register.vue";
@@ -33,6 +34,7 @@ export const getAdminRouter = (Galaxy, options) => {
"(/)admin(/)form(/)(:form_id)": "show_form",
"(/)admin/data_tables": "show_data_tables",
"(/)admin/data_types": "show_data_types",
"(/)admin/jobs": "show_jobs",
"(/)admin/data_manager*path": "show_data_manager",
"*notFound": "not_found"
},
@@ -115,6 +117,10 @@ export const getAdminRouter = (Galaxy, options) => {
this._display_vue_helper(DataTypes);
},
show_jobs: function() {
this._display_vue_helper(Jobs);
},
show_error_stack: function() {
this._display_vue_helper(ErrorStack);
},
@@ -39,6 +39,7 @@ var AdminPanel = Backbone.View.extend({
{
title: _l("Manage jobs"),
url: "admin/jobs",
target: "__use_router__",
id: "admin-link-jobs"
},
{
+1
View File
@@ -101,6 +101,7 @@ def app_factory(global_conf, load_app_kwds={}, **kwargs):
webapp.add_client_route('/admin/data_tables', 'admin')
webapp.add_client_route('/admin/data_types', 'admin')
webapp.add_client_route('/admin/jobs', 'admin')
webapp.add_client_route('/admin/data_manager{path_info:.*}', 'admin')
webapp.add_client_route('/admin/error_stack', 'admin')
webapp.add_client_route('/admin/users', 'admin')
+58 -46
View File
@@ -1600,14 +1600,25 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
return {'message' : 'User \'%s\' has been updated with %d associated roles and %d associated groups (private roles are not displayed).' % (user.email, len(in_roles) - 1, len(in_groups))}
@web.expose
@web.json
@web.require_admin
def jobs(self, trans, stop=[], stop_msg=None, cutoff=180, job_lock=None, ajl_submit=None, **kwd):
def jobs_control(self, trans, job_lock=None, **kwd):
if job_lock is not None:
job_lock = True if job_lock == 'true' else False
galaxy.queue_worker.send_control_task(trans.app, 'admin_job_lock', kwargs={'job_lock': job_lock})
job_lock = trans.app.job_manager.job_lock
return {'job_lock': job_lock}
@web.expose
@web.json
@web.require_admin
def jobs_list(self, trans, stop=[], stop_msg=None, cutoff=180, **kwd):
deleted = []
msg = None
status = None
message = kwd.get('message', '')
status = kwd.get('status', 'info')
job_ids = util.listify(stop)
if job_ids and stop_msg in [None, '']:
msg = 'Please enter an error message to display to the user describing why the job was terminated'
message = 'Please enter an error message to display to the user describing why the job was terminated'
status = 'error'
elif job_ids:
if stop_msg[-1] not in PUNCTUATION:
@@ -1624,24 +1635,14 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
trans.app.job_manager.stop(job, message=error_msg)
deleted.append(str(job_id))
if deleted:
msg = 'Queued job'
message = 'Queued job'
if len(deleted) > 1:
msg += 's'
msg += ' for deletion: '
msg += ', '.join(deleted)
message += 's'
message += ' for deletion: '
message += ', '.join(deleted)
status = 'done'
trans.sa_session.flush()
if ajl_submit:
if job_lock == 'on':
galaxy.queue_worker.send_control_task(trans.app, 'admin_job_lock',
kwargs={'job_lock': True})
job_lock = True
else:
galaxy.queue_worker.send_control_task(trans.app, 'admin_job_lock',
kwargs={'job_lock': False})
job_lock = False
else:
job_lock = trans.app.job_manager.job_lock
job_lock = trans.app.job_manager.job_lock
cutoff_time = datetime.utcnow() - timedelta(seconds=int(cutoff))
jobs = trans.sa_session.query(trans.app.model.Job) \
.filter(and_(trans.app.model.Job.table.c.update_time < cutoff_time,
@@ -1655,33 +1656,44 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
or_(trans.app.model.Job.state == trans.app.model.Job.states.ERROR,
trans.app.model.Job.state == trans.app.model.Job.states.OK))) \
.order_by(trans.app.model.Job.table.c.update_time.desc()).all()
last_updated = {}
for job in jobs:
delta = datetime.utcnow() - job.update_time
if delta.days > 0:
last_updated[job.id] = '%s hours' % (delta.days * 24 + int(delta.seconds / 60 / 60))
elif delta > timedelta(minutes=59):
last_updated[job.id] = '%s hours' % int(delta.seconds / 60 / 60)
else:
last_updated[job.id] = '%s minutes' % int(delta.seconds / 60)
finished = {}
for job in recent_jobs:
delta = datetime.utcnow() - job.update_time
if delta.days > 0:
finished[job.id] = '%s hours' % (delta.days * 24 + int(delta.seconds / 60 / 60))
elif delta > timedelta(minutes=59):
finished[job.id] = '%s hours' % int(delta.seconds / 60 / 60)
else:
finished[job.id] = '%s minutes' % int(delta.seconds / 60)
return trans.fill_template('/admin/jobs.mako',
jobs=jobs,
recent_jobs=recent_jobs,
last_updated=last_updated,
finished=finished,
cutoff=cutoff,
msg=msg,
status=status,
job_lock=job_lock)
def prepare_jobs_list(jobs):
res = []
for job in jobs:
delta = datetime.utcnow() - job.update_time
update_time = ""
if delta.days > 0:
update_time = '%s hours ago' % (delta.days * 24 + int(delta.seconds / 60 / 60))
elif delta > timedelta(minutes=59):
update_time = '%s hours ago' % int(delta.seconds / 60 / 60)
else:
update_time = '%s minutes ago' % int(delta.seconds / 60)
inputs = ""
try:
inputs = ", ".join(['{} {}'.format(da.dataset.id, da.dataset.state) for da in job.input_datasets])
except Exception:
inputs = 'Unable to determine inputs'
res.append({
'job_info': {
'id': job.id,
'info_url': "{}?jobid={}".format(web.url_for(controller="admin", action="job_info"), job.id)
},
'user': job.history.user.email if job.history and job.history.user else 'anonymous',
'update_time': update_time,
'tool_id': job.tool_id,
'state': job.state,
'input_dataset': inputs,
'command_line': job.command_line,
'job_runner_name': job.job_runner_name,
'job_runner_external_id': job.job_runner_external_id
})
return res
return {'jobs': prepare_jobs_list(jobs),
'recent_jobs': prepare_jobs_list(recent_jobs),
'cutoff': cutoff,
'message': message,
'status': status,
'job_lock': job_lock}
@web.expose
@web.require_admin
-209
View File
@@ -1,209 +0,0 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
function toggle_all(source) {
// sets all checkboxes in source's parent form to match source element.
$.each($(source).closest("form").find(":checkbox"), function(i, v){
v.checked = source.checked;
});
}
</script>
</%def>
<%def name="title()">Jobs</%def>
<h2>Jobs</h2>
%if message:
${render_msg( message, status )}
%endif
<p>
Unfinished and recently finished jobs are displayed on this page. The
'cutoff' input box will do two things -- it will limit the display of
unfinished jobs to only those jobs that have not had their job state
updated recently, and it will limit the recently finished jobs list to only
displaying jobs that have finished since the cutoff.
</p>
<p>
If any jobs are displayed, you may choose to stop them. Your stop message
will be displayed to the user as: "This job was stopped by an
administrator: <b>&lt;YOUR MESSAGE&gt;</b> For more information or help,
report this error".
</p>
%if jobs:
<form name="jobs" action="${h.url_for(controller='admin', action='jobs')}" method="POST">
<h4>
Unfinished Jobs: These jobs are unfinished and have had their state updated in the previous ${cutoff} seconds.
</h4>
<table class="manage-table colored" border="0" cellspacing="0" cellpadding="0" width="100%">
<tr class="header">
<td><input type="checkbox" onClick="toggle_all(this)"/></td>
<td>Job ID</td>
<td>User</td>
<td>Last Update</td>
<td>Tool</td>
<td>State</td>
<td>Inputs</td>
<td>Command Line</td>
<td>Job Runner</td>
<td>PID/Cluster ID</td>
</tr>
%for job in jobs:
<td>
<input type="checkbox" name="stop" value="${job.id}"/>
</td>
<td>${job.id}</td>
%if job.history and job.history.user:
<td>${job.history.user.email|h}</td>
%else:
<td>anonymous</td>
%endif
<td>${last_updated[job.id]} ago</td>
<td>${job.tool_id|h}</td>
<td>${job.state}</td>
<%
try:
inputs = ", ".join( [ '%s&nbsp;%s' % ( da.dataset.id, da.dataset.state ) for da in job.input_datasets ] )
except:
inputs = 'Unable to determine inputs'
%>
<td>${inputs}</td>
<td>${job.command_line|h}</td>
<td>${job.job_runner_name|h}</td>
<td>${job.job_runner_external_id}</td>
</tr>
%endfor
</table>
<p/>
<div class="card">
<div class="card-header">
Stop Jobs
</div>
<div class="card-body">
<div class="form-row">
<label>
Stop message:
</label>
<div class="form-row-input">
<input type="text" name="stop_msg" size="40"/>
</div>
<div class="toolParamHelp" style="clear: both;">
to be displayed to the user
</div>
</div>
<div class="form-row">
<input type="submit" class="primary-button" name="submit" value="Submit">
</div>
</div>
</div>
<p/>
</form>
%else:
<div class="infomessage">There are no unfinished jobs to show with current cutoff time.</div>
<p/>
%endif
%if recent_jobs:
<h4>
Recent Jobs: These jobs have completed in the previous ${cutoff} seconds.
</h4>
<table class="manage-table colored" border="0" cellspacing="0" cellpadding="0" width="100%">
<tr class="header">
<td>Job ID</td>
<td>User</td>
<td>Finished</td>
<td>Tool</td>
<td>State</td>
<td>Inputs</td>
<td>Command Line</td>
<td>Job Runner</td>
<td>PID/Cluster ID</td>
</tr>
%for job in recent_jobs:
<td><a href="${h.url_for( controller="admin", action="job_info" )}?jobid=${job.id}">${job.id}</a></td>
%if job.history and job.history.user:
<td>${job.history.user.email|h}</td>
%else:
<td>anonymous</td>
%endif
<td>${finished[job.id]} ago</td>
<td>${job.tool_id|h}</td>
<td>${job.state}</td>
<%
try:
inputs = ", ".join( [ '%s&nbsp;%s' % ( da.dataset.id, da.dataset.state ) for da in job.input_datasets ] )
except:
inputs = 'Unable to determine inputs'
%>
<td>${inputs}</td>
<td>${job.command_line|h}</td>
<td>${job.job_runner_name|h}</td>
<td>${job.job_runner_external_id|h}</td>
</tr>
%endfor
</table>
<p/>
%else:
<div class="infomessage">There are no recently finished jobs to show with current cutoff time.</div>
<p/>
%endif
<form name="jobs" action="${h.url_for(controller='admin', action='jobs')}" method="POST">
<div class="card">
<div class="card-header">
Update Jobs
</div>
<div class="card-body">
<div class="form-row">
<label>
Cutoff:
</label>
<div class="form-row-input">
<input type="text" name="cutoff" size="4" value="${cutoff}"/>
</div>
<div class="toolParamHelp" style="clear: both;">
In seconds
</div>
</div>
<div class="form-row">
<input type="submit" class="primary-button" name="submit" value="Refresh">
</div>
</div>
</div>
</form>
<form name="jobs" action="${h.url_for(controller='admin', action='jobs')}" method="POST">
<p/>
<div class="card">
<div class="card-header">
Administrative Job Lock
</div>
<div class="card-body">
<div class="form-row">
<input type="hidden" name="ajl_submit" value="True"/>
%if job_lock==True:
<p>Job dispatching is currently <strong>locked</strong>.</p>
<label>
<input type='checkbox' name='job_lock' checked='checked' />
Prevent jobs from dispatching.
</label>
%else:
<p>Job dispatching is currently <strong>unlocked</strong>.</p>
<label>
<input type='checkbox' name='job_lock' />
Prevent jobs from dispatching.
</label>
%endif
</div>
<div class="form-row">
<input type="submit" class="primary-button" name="submit" value="Update">
</div>
</div>
</div>
</form>