mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #11868 from mvdbeek/handler_column_in_admin_jobs
Add handler column in admin jobs, remove webcontroller
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"bootstrap-vue": "^2.15.0",
|
||||
"citation-js": "^0.5.0-alpha.5",
|
||||
"d3": "3",
|
||||
"date-fns": "^2.21.1",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"deep-diff": "^1.0.2",
|
||||
"deep-equal": "^2.0.5",
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
<UtcDate :date="job.update_time" mode="pretty" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="job && includeTimes && jobIsTerminal">
|
||||
<td>Time To Finish</td>
|
||||
<td id="runtime">
|
||||
{{ runTime }}
|
||||
</td>
|
||||
</tr>
|
||||
<code-row id="command-line" v-if="job" :code-label="'Command Line'" :code-item="job.command_line" />
|
||||
<code-row id="stdout" v-if="job" :code-label="'Tool Standard Output'" :code-item="job.tool_stdout" />
|
||||
<code-row id="stderr" v-if="job" :code-label="'Tool Standard Error'" :code-item="job.tool_stderr" />
|
||||
@@ -45,6 +51,7 @@
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<slot></slot>
|
||||
<tr v-if="job && job.id">
|
||||
<td>Job API ID:</td>
|
||||
<td id="encoded-job-id">{{ job.id }} <decoded-id :id="job.id" /></td>
|
||||
@@ -67,6 +74,8 @@ import DecodedId from "../DecodedId.vue";
|
||||
import CodeRow from "./CodeRow.vue";
|
||||
import UtcDate from "components/UtcDate";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import JOB_STATES_MODEL from "mvc/history/job-states-model";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -90,8 +99,15 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
job: function () {
|
||||
const job = this.$store.getters.job(this.job_id);
|
||||
return job;
|
||||
return this.$store.getters.job(this.job_id);
|
||||
},
|
||||
runTime: function () {
|
||||
return formatDuration(
|
||||
intervalToDuration({ start: new Date(this.job.create_time), end: new Date(this.job.update_time) })
|
||||
);
|
||||
},
|
||||
jobIsTerminal() {
|
||||
return !JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(this.job.state);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<h5>Job Parameters</h5>
|
||||
<job-information :job_id="job.id" :includeTimes="true">
|
||||
<tr v-if="job.traceback">
|
||||
<td>Traceback</td>
|
||||
<td>
|
||||
<code-row :code-label="'Traceback'" :code-item="job.traceback" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="job.info">
|
||||
<td>Info</td>
|
||||
<td>
|
||||
<code-row :code-label="'Traceback'" :code-item="job.traceback" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="job.remote_host">
|
||||
<td>Remote Host</td>
|
||||
<td>
|
||||
{{ job.remote_host }}
|
||||
</td>
|
||||
</tr>
|
||||
</job-information>
|
||||
<br />
|
||||
<h3>Job Parameters</h3>
|
||||
<job-parameters :job-id="job.id" :include-title="false" />
|
||||
<h5>Job Metrics</h5>
|
||||
<br />
|
||||
<h3>Job Metrics</h3>
|
||||
<job-metrics :job-id="job.id" :include-title="false" />
|
||||
</b-card>
|
||||
</template>
|
||||
@@ -10,9 +32,13 @@
|
||||
<script>
|
||||
import { JobMetrics } from "components/JobMetrics";
|
||||
import { JobParameters } from "components/JobParameters";
|
||||
import JobInformation from "components/JobInformation/JobInformation";
|
||||
import CodeRow from "components/JobInformation/CodeRow.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CodeRow,
|
||||
JobInformation,
|
||||
JobMetrics,
|
||||
JobParameters,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const commonJobFields = [
|
||||
{ key: "id", label: "Encoded Job ID", sortable: true },
|
||||
{ key: "decoded_job_id", label: "Decoded Job ID", sortable: true },
|
||||
{ key: "user_email" },
|
||||
{ key: "tool_id", label: "Tool", tdClass: ["break-word"] },
|
||||
{ key: "state" },
|
||||
{ key: "handler" },
|
||||
{ key: "job_runner_name", label: "Job Runner" },
|
||||
{ key: "external_id", label: "PID/Cluster ID", sortable: true },
|
||||
];
|
||||
@@ -57,7 +57,7 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
<transition name="fade">
|
||||
<b-form v-if="jobsItemsComputed.length && selectedStopJobIds.length" @submit.prevent="onStopJobs">
|
||||
<b-form v-if="unfinishedJobs.length && selectedStopJobIds.length" @submit.prevent="onStopJobs">
|
||||
<b-form-group label="Stop Selected Jobs" description="Stop message will be displayed to the user">
|
||||
<b-input-group>
|
||||
<b-form-input id="stop-message" v-model="stopMessage" placeholder="Stop message" required>
|
||||
@@ -70,20 +70,20 @@
|
||||
</b-form>
|
||||
</transition>
|
||||
<h4>Unfinished Jobs</h4>
|
||||
<b-alert v-if="!jobsItemsComputed.length" variant="secondary" show>
|
||||
<b-alert v-if="!unfinishedJobs.length" variant="secondary" show>
|
||||
There are no unfinished jobs to show with current cutoff time of {{ cutoffMin }} minutes.
|
||||
</b-alert>
|
||||
<b-table
|
||||
v-else
|
||||
:fields="jobsFieldsComputed"
|
||||
:items="jobsItemsComputed"
|
||||
v-model="jobsItemsModel"
|
||||
:fields="unfinishedJobFields"
|
||||
:items="unfinishedJobs"
|
||||
:filter="filter"
|
||||
hover
|
||||
responsive
|
||||
striped
|
||||
caption-top
|
||||
@row-clicked="showRowDetails"
|
||||
@row-clicked="toggleDetails"
|
||||
:busy="busy"
|
||||
>
|
||||
<template v-slot:table-caption>
|
||||
@@ -109,53 +109,38 @@
|
||||
<template v-slot:cell(update_time)="data">
|
||||
<utc-date :date="data.value" mode="elapsed" />
|
||||
</template>
|
||||
<template v-slot:cell(job_info)="data">
|
||||
<b-link :href="data.value.info_url" @click.prevent="clickJobInfo(data.value.id)">
|
||||
{{ data.value.id }}
|
||||
</b-link>
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<job-details :job="row.item" />
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<h4>Finished Jobs</h4>
|
||||
<b-alert v-if="!recentJobsItemsComputed.length" variant="secondary" show>
|
||||
<b-alert v-if="!finishedJobs.length" variant="secondary" show>
|
||||
There are no recently finished jobs to show with current cutoff time of {{ cutoffMin }} minutes.
|
||||
</b-alert>
|
||||
|
||||
<b-table
|
||||
v-else
|
||||
:fields="recentJobsFieldsComputed"
|
||||
:items="recentJobsItemsComputed"
|
||||
:fields="finishedJobFields"
|
||||
:items="finishedJobs"
|
||||
:filter="filter"
|
||||
hover
|
||||
responsive
|
||||
striped
|
||||
caption-top
|
||||
@row-clicked="showRowDetails"
|
||||
@row-clicked="toggleDetails"
|
||||
:busy="busy"
|
||||
>
|
||||
<template v-slot:table-caption>
|
||||
These jobs have completed in the previous {{ cutoffMin }} minutes.
|
||||
</template>
|
||||
<template v-slot:cell(job_info)="data">
|
||||
<b-link :href="data.value.info_url" @click.prevent="clickJobInfo(data.value.id)">
|
||||
{{ data.value.id }}
|
||||
</b-link>
|
||||
</template>
|
||||
<template v-slot:cell(update_time)="data">
|
||||
<utc-date :date="data.value" mode="elapsed" />
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<job-details :command-line="row.item.command_line" :job-id="row.item.id" />
|
||||
<job-details :job="row.item" />
|
||||
</template>
|
||||
</b-table>
|
||||
<b-modal ref="job-info-modal" scrollable hide-header ok-only @hidden="resetModalContents">
|
||||
<div class="info-frame-container">
|
||||
<iframe :src="selectedJobUrl"></iframe>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -166,6 +151,8 @@ import UtcDate from "components/UtcDate";
|
||||
import axios from "axios";
|
||||
import JobDetails from "./JobDetails";
|
||||
import JobLock from "./JobLock";
|
||||
import JOB_STATES_MODEL from "mvc/history/job-states-model";
|
||||
import { commonJobFields } from "./JobFields";
|
||||
|
||||
function cancelJob(jobId, message) {
|
||||
const url = `${getAppRoot()}api/jobs/${jobId}`;
|
||||
@@ -176,16 +163,15 @@ export default {
|
||||
components: { UtcDate, JobDetails, JobLock },
|
||||
data() {
|
||||
return {
|
||||
jobsItems: [],
|
||||
jobs: [],
|
||||
finishedJobs: [],
|
||||
unfinishedJobs: [],
|
||||
jobsItemsModel: [],
|
||||
recentJobsItems: [],
|
||||
jobsFields: [
|
||||
{ key: "job_info", label: "Job ID", sortable: true },
|
||||
{ key: "user" },
|
||||
{ key: "tool_id", label: "Tool", tdClass: ["break-word"] },
|
||||
{ key: "state" },
|
||||
{ key: "job_runner_name", label: "Job Runner" },
|
||||
{ key: "job_runner_external_id", label: "PID/Cluster ID", sortable: true },
|
||||
finishedJobFields: [...commonJobFields, { key: "update_time", label: "Finished", sortable: true }],
|
||||
unfinishedJobFields: [
|
||||
{ key: "selected", label: "" },
|
||||
...commonJobFields,
|
||||
{ key: "update_time", label: "Last Update", sortable: true },
|
||||
],
|
||||
selectedStopJobIds: [],
|
||||
selectedJobId: null,
|
||||
@@ -213,20 +199,34 @@ export default {
|
||||
this.allSelected = false;
|
||||
}
|
||||
},
|
||||
jobs(newVal) {
|
||||
const unfinishedJobs = [];
|
||||
const finishedJobs = [];
|
||||
newVal.forEach((item) => {
|
||||
item._cellVariants = { state: this.translateState(item.state) };
|
||||
if (JOB_STATES_MODEL.NON_TERMINAL_STATES.includes(item.state)) {
|
||||
unfinishedJobs.push(item);
|
||||
} else {
|
||||
finishedJobs.push(item);
|
||||
}
|
||||
});
|
||||
this.unfinishedJobs = unfinishedJobs;
|
||||
this.finishedJobs = finishedJobs;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.busy = true;
|
||||
let params = [];
|
||||
const cutoff = Math.floor(this.cutoffMin * 60);
|
||||
params.push(`cutoff=${cutoff}`);
|
||||
const cutoff = Math.floor(this.cutoffMin);
|
||||
const dateRangeMin = new Date(Date.now() - cutoff * 60 * 1000).toISOString();
|
||||
params.push(`date_range_min=${dateRangeMin}`);
|
||||
params.push("view=admin_job_list");
|
||||
params = params.join("&");
|
||||
axios
|
||||
.get(`${getAppRoot()}admin/jobs_list?${params}`)
|
||||
.get(`${getAppRoot()}api/jobs?${params}`)
|
||||
.then((response) => {
|
||||
this.jobsItems = response.data.jobs;
|
||||
this.recentJobsItems = response.data.recent_jobs;
|
||||
this.cutoffMin = Math.floor(response.data.cutoff / 60);
|
||||
this.jobs = response.data;
|
||||
this.message = response.data.message;
|
||||
this.status = response.data.status;
|
||||
this.loading = false;
|
||||
@@ -248,17 +248,9 @@ export default {
|
||||
this.stopMessage = "";
|
||||
});
|
||||
},
|
||||
clickJobInfo(id) {
|
||||
this.selectedJobId = id;
|
||||
this.$refs["job-info-modal"].show();
|
||||
},
|
||||
resetModalContents() {
|
||||
this.selectedJobId = null;
|
||||
},
|
||||
showRowDetails(row, index, e) {
|
||||
if (e.target.nodeName != "A") {
|
||||
row._showDetails = !row._showDetails;
|
||||
}
|
||||
|
||||
toggleDetails(item) {
|
||||
this.$set(item, "_showDetails", !item._showDetails);
|
||||
},
|
||||
translateState(state) {
|
||||
const translateDict = {
|
||||
@@ -280,10 +272,6 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
computeFields(fields) {
|
||||
const f = Array.from(fields).slice(0);
|
||||
return f;
|
||||
},
|
||||
toggleAll(checked) {
|
||||
this.selectedStopJobIds = checked ? this.jobsItemsModel.reduce((acc, j) => [...acc, j["id"]], []) : [];
|
||||
},
|
||||
@@ -305,28 +293,6 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobsItemsComputed() {
|
||||
return this.computeItems(this.jobsItems);
|
||||
},
|
||||
recentJobsItemsComputed() {
|
||||
return this.computeItems(this.recentJobsItems);
|
||||
},
|
||||
jobsFieldsComputed() {
|
||||
const f = this.jobsFields.slice(0);
|
||||
f.splice(0, 0, { key: "selected", label: "" });
|
||||
f.splice(2, 0, { key: "update_time", label: "Last Update", sortable: true });
|
||||
return this.computeFields(f);
|
||||
},
|
||||
recentJobsFieldsComputed() {
|
||||
const f = this.jobsFields.slice(0);
|
||||
f.splice(2, 0, { key: "update_time", label: "Finished", sortable: true });
|
||||
return this.computeFields(f);
|
||||
},
|
||||
selectedJobUrl() {
|
||||
return `${getAppRoot()}admin/job_info?jobid=${this.selectedJobId}`;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import axios from "axios";
|
||||
|
||||
const getters = {
|
||||
job: (state) => (jobId) => {
|
||||
return state.job[jobId] || [];
|
||||
return state.job[jobId];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4886,6 +4886,11 @@ data-urls@^2.0.0:
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
date-fns@^2.21.1:
|
||||
version "2.21.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0"
|
||||
integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==
|
||||
|
||||
date-format@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"
|
||||
|
||||
@@ -1131,14 +1131,25 @@ class Job(JobLike, UsesCreateAndUpdateTime, Dictifiable, RepresentById):
|
||||
return job_attrs
|
||||
|
||||
def to_dict(self, view='collection', system_details=False):
|
||||
rval = super().to_dict(view=view)
|
||||
if view == 'admin_job_list':
|
||||
rval = super().to_dict(view='collection')
|
||||
else:
|
||||
rval = super().to_dict(view=view)
|
||||
rval['tool_id'] = self.tool_id
|
||||
rval['history_id'] = self.history_id
|
||||
if system_details:
|
||||
if system_details or view == 'admin_job_list':
|
||||
# System level details that only admins should have.
|
||||
rval['external_id'] = self.job_runner_external_id
|
||||
rval['command_line'] = self.command_line
|
||||
|
||||
if view == 'admin_job_list':
|
||||
rval['user_email'] = self.user.email if self.user else None
|
||||
rval['handler'] = self.handler
|
||||
rval['job_runner_name'] = self.job_runner_name
|
||||
rval['info'] = self.info
|
||||
rval['traceback'] = self.traceback
|
||||
rval['session_id'] = self.session_id
|
||||
if self.galaxy_session and self.galaxy_session.remote_host:
|
||||
rval['remote_host'] = self.galaxy_session.remote_host
|
||||
if view == 'element':
|
||||
param_dict = {p.name: p.value for p in self.parameters}
|
||||
rval['params'] = param_dict
|
||||
|
||||
@@ -121,6 +121,9 @@ class JobController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
:type invocation_id: string
|
||||
:param invocation_id: limit listing of jobs to those that match the invocation_id. If none, all are returned.
|
||||
|
||||
:type view: string
|
||||
:param view: Determines columns to return. Defaults to 'collection'.
|
||||
|
||||
:rtype: list
|
||||
:returns: list of dictionaries containing summary job information
|
||||
"""
|
||||
@@ -128,6 +131,11 @@ class JobController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
is_admin = trans.user_is_admin
|
||||
user_details = kwd.get('user_details', False)
|
||||
user_id = kwd.get('user_id', None)
|
||||
view = kwd.get('view', 'collection')
|
||||
if view not in ('collection', 'admin_job_list'):
|
||||
raise exceptions.RequestParameterInvalidException(f"view parameter '{view} is invalid")
|
||||
if view == 'admin_job_list' and not is_admin:
|
||||
raise exceptions.AdminRequiredException("Only admins can use the admin_job_list view")
|
||||
|
||||
if user_id:
|
||||
decoded_user_id = self.decode_id(user_id)
|
||||
@@ -197,8 +205,10 @@ class JobController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
|
||||
out = []
|
||||
for job in query.all():
|
||||
job_dict = job.to_dict('collection', system_details=is_admin)
|
||||
job_dict = job.to_dict(view, system_details=is_admin)
|
||||
j = self.encode_all_ids(trans, job_dict, True)
|
||||
if view == 'admin_job_list':
|
||||
j['decoded_job_id'] = job.id
|
||||
if user_details:
|
||||
j['user_email'] = job.user.email
|
||||
out.append(j)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import imp
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import and_, false, or_
|
||||
from sqlalchemy import false
|
||||
|
||||
from galaxy import (
|
||||
model,
|
||||
@@ -1519,58 +1518,6 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
trans.sa_session.refresh(user)
|
||||
return {'message': f"User '{user.email}' has been updated with {len(in_roles) - 1} associated roles and {len(in_groups)} associated groups (private roles are not displayed)."}
|
||||
|
||||
@web.expose
|
||||
@web.json
|
||||
@web.require_admin
|
||||
def jobs_list(self, trans, cutoff=180, **kwd):
|
||||
message = kwd.get('message', '')
|
||||
status = kwd.get('status', 'info')
|
||||
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,
|
||||
or_(trans.app.model.Job.state == trans.app.model.Job.states.NEW,
|
||||
trans.app.model.Job.state == trans.app.model.Job.states.QUEUED,
|
||||
trans.app.model.Job.state == trans.app.model.Job.states.RUNNING,
|
||||
trans.app.model.Job.state == trans.app.model.Job.states.UPLOAD))) \
|
||||
.order_by(trans.app.model.Job.table.c.update_time.desc()).all()
|
||||
recent_jobs = trans.sa_session.query(trans.app.model.Job) \
|
||||
.filter(and_(trans.app.model.Job.table.c.update_time > cutoff_time,
|
||||
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()
|
||||
|
||||
def prepare_jobs_list(jobs):
|
||||
res = []
|
||||
for job in jobs:
|
||||
res.append({
|
||||
'job_info': {
|
||||
'id': job.id,
|
||||
},
|
||||
'id': trans.security.encode_id(job.id),
|
||||
'user': job.history.user.email if job.history and job.history.user else 'anonymous',
|
||||
'update_time': job.update_time.isoformat(),
|
||||
'tool_id': job.tool_id,
|
||||
'state': job.state,
|
||||
'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}
|
||||
|
||||
@web.expose
|
||||
@web.require_admin
|
||||
def job_info(self, trans, jobid=None):
|
||||
job = None
|
||||
if jobid is not None:
|
||||
job = trans.sa_session.query(trans.app.model.Job).get(jobid)
|
||||
return trans.fill_template('/webapps/reports/job_info.mako',
|
||||
job=job)
|
||||
|
||||
@web.expose
|
||||
@web.require_admin
|
||||
def manage_tool_dependencies(self,
|
||||
|
||||
@@ -46,6 +46,17 @@ class JobsApiTestCase(ApiTestCase, TestsTools):
|
||||
job = jobs[0]
|
||||
self._assert_has_keys(job, "command_line", "external_id")
|
||||
|
||||
@uses_test_history(require_new=True)
|
||||
def test_admin_job_list(self, history_id):
|
||||
self.__history_with_new_dataset(history_id)
|
||||
jobs_response = self._get("jobs?view=admin_job_list", admin=False)
|
||||
assert jobs_response.status_code == 403
|
||||
assert jobs_response.json()['err_msg'] == 'Only admins can use the admin_job_list view'
|
||||
|
||||
jobs = self._get("jobs?view=admin_job_list", admin=True).json()
|
||||
job = jobs[0]
|
||||
self._assert_has_keys(job, "command_line", "external_id", 'handler')
|
||||
|
||||
@uses_test_history(require_new=True)
|
||||
def test_index_state_filter(self, history_id):
|
||||
# Initial number of ok jobs
|
||||
|
||||
Reference in New Issue
Block a user