diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js
index abc8be4d625..c666d664310 100644
--- a/client/galaxy/scripts/bundleEntries.js
+++ b/client/galaxy/scripts/bundleEntries.js
@@ -100,6 +100,8 @@ export const chartUtilities = {
export { initMasthead } from "components/Masthead/initMasthead";
export { panelManagement } from "onload/globalInits/panelManagement";
export { mountMakoTags } from "components/Tags";
+export { mountJobMetrics } from "components/JobMetrics";
+export { mountJobParameters } from "components/JobParameters";
// Used in common.mako
export { default as store } from "storemodern";
diff --git a/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue b/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue
new file mode 100644
index 00000000000..67fcd9c829e
--- /dev/null
+++ b/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue
@@ -0,0 +1,80 @@
+
+
+
Job Metrics
+
+
{{ plugin }}
+
+
+
+ | {{ metricTitle }} |
+ {{ metricValue }} |
+
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/JobMetrics/index.js b/client/galaxy/scripts/components/JobMetrics/index.js
new file mode 100644
index 00000000000..416a80e8339
--- /dev/null
+++ b/client/galaxy/scripts/components/JobMetrics/index.js
@@ -0,0 +1,4 @@
+export { default as JobMetrics } from "./JobMetrics";
+
+// functions for mounting job metrics in non-Vue environments
+export { mountJobMetrics } from "./mount";
diff --git a/client/galaxy/scripts/components/JobMetrics/mount.js b/client/galaxy/scripts/components/JobMetrics/mount.js
new file mode 100644
index 00000000000..52206d987fe
--- /dev/null
+++ b/client/galaxy/scripts/components/JobMetrics/mount.js
@@ -0,0 +1,19 @@
+/**
+ * Endpoint for mounting job metrics from non-Vue environment.
+ */
+import $ from "jquery";
+import Vue from "vue";
+import JobMetrics from "./JobMetrics.vue";
+
+export const mountJobMetrics = (propsData = {}) => {
+ $(".job-metrics").each((index, el) => {
+ const jobId = $(el).attr("job_id");
+ const datasetId = $(el).attr("dataset_id");
+ const datasetType = $(el).attr("dataset_type") || "hda";
+ const component = Vue.extend(JobMetrics);
+ propsData.jobId = jobId;
+ propsData.datasetId = datasetId;
+ propsData.datasetType = datasetType;
+ return new component({ propsData: propsData }).$mount(el);
+ });
+};
diff --git a/client/galaxy/scripts/components/JobParameters/JobParameters.vue b/client/galaxy/scripts/components/JobParameters/JobParameters.vue
new file mode 100644
index 00000000000..fc9a94b6f0d
--- /dev/null
+++ b/client/galaxy/scripts/components/JobParameters/JobParameters.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/JobParameters/index.js b/client/galaxy/scripts/components/JobParameters/index.js
new file mode 100644
index 00000000000..8d76729387f
--- /dev/null
+++ b/client/galaxy/scripts/components/JobParameters/index.js
@@ -0,0 +1,4 @@
+export { default as JobParameters } from "./JobParameters";
+
+// functions for mounting job parameters display in non-Vue environments
+export { mountJobParameters } from "./mount";
diff --git a/client/galaxy/scripts/components/JobParameters/mount.js b/client/galaxy/scripts/components/JobParameters/mount.js
new file mode 100644
index 00000000000..4e7e9f67fb6
--- /dev/null
+++ b/client/galaxy/scripts/components/JobParameters/mount.js
@@ -0,0 +1,19 @@
+/**
+ * Endpoint for mounting job parameters from non-Vue environment.
+ */
+import $ from "jquery";
+import Vue from "vue";
+import JobParameters from "./JobParameters.vue";
+
+export const mountJobParameters = (propsData = {}) => {
+ $(".job-parameters").each((index, el) => {
+ const jobId = $(el).attr("job_id");
+ const datasetId = $(el).attr("dataset_id");
+ const datasetType = $(el).attr("dataset_type") || "hda";
+ const component = Vue.extend(JobParameters);
+ propsData.jobId = jobId;
+ propsData.datasetId = datasetId;
+ propsData.datasetType = datasetType;
+ return new component({ propsData: propsData }).$mount(el);
+ });
+};
diff --git a/lib/galaxy/managers/jobs.py b/lib/galaxy/managers/jobs.py
index 8941171de7a..3940e20c4eb 100644
--- a/lib/galaxy/managers/jobs.py
+++ b/lib/galaxy/managers/jobs.py
@@ -8,8 +8,13 @@ from sqlalchemy.orm import aliased
from sqlalchemy.sql import select
from galaxy import model
-from galaxy.exceptions import RequestParameterInvalidException
+from galaxy.exceptions import (
+ ItemAccessibilityException,
+ ObjectNotFound,
+ RequestParameterInvalidException,
+)
from galaxy.managers.collections import DatasetCollectionManager
+from galaxy.managers.datasets import DatasetManager
from galaxy.managers.hdas import HDAManager
from galaxy.managers.lddas import LDDAManager
from galaxy.util import (
@@ -39,6 +44,27 @@ def get_path_key(path_tuple):
return path_key
+class JobManager(object):
+
+ def __init__(self, app):
+ self.app = app
+ self.dataset_manager = DatasetManager(app)
+
+ def get_accessible_job(self, trans, decoded_job_id):
+ job = trans.sa_session.query(trans.app.model.Job).filter(trans.app.model.Job.id == decoded_job_id).first()
+ if job is None:
+ raise ObjectNotFound()
+ belongs_to_user = (job.user == trans.user) if job.user else (job.session_id == trans.get_galaxy_session().id)
+ if not trans.user_is_admin and not belongs_to_user:
+ # Check access granted via output datasets.
+ if not job.output_datasets:
+ raise ItemAccessibilityException("Job has no output datasets.")
+ for data_assoc in job.output_datasets:
+ if not self.dataset_manager.is_accessible(data_assoc.dataset.dataset, trans.user):
+ raise ItemAccessibilityException("You are not allowed to rerun this job.")
+ return job
+
+
class JobSearch(object):
"""Search for jobs using tool inputs or other jobs"""
def __init__(self, app):
diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py
index f42c83a998b..1f54f4e7aea 100644
--- a/lib/galaxy/webapps/galaxy/api/jobs.py
+++ b/lib/galaxy/webapps/galaxy/api/jobs.py
@@ -12,24 +12,25 @@ from sqlalchemy import or_
from galaxy import exceptions
from galaxy import model
from galaxy import util
-from galaxy.managers.datasets import DatasetManager
-from galaxy.managers.jobs import JobSearch
+from galaxy.managers.jobs import JobManager, JobSearch
from galaxy.web import (
expose_api,
expose_api_anonymous,
)
-from galaxy.web.base.controller import BaseAPIController
-from galaxy.web.base.controller import UsesLibraryMixinItems
+from galaxy.web.base.controller import (
+ BaseAPIController,
+ UsesVisualizationMixin
+)
from galaxy.work.context import WorkRequestContext
log = logging.getLogger(__name__)
-class JobController(BaseAPIController, UsesLibraryMixinItems):
+class JobController(BaseAPIController, UsesVisualizationMixin):
def __init__(self, app):
super(JobController, self).__init__(app)
- self.dataset_manager = DatasetManager(app)
+ self.job_manager = JobManager(app)
self.job_search = JobSearch(app)
@expose_api
@@ -166,7 +167,7 @@ class JobController(BaseAPIController, UsesLibraryMixinItems):
raw_value=str(metric_value),
)
- job_dict['job_metrics'] = [metric_to_dict(metric) for metric in job.metrics]
+ job_dict['job_metrics'] = self._metrics_as_dict(trans, job)
return job_dict
@expose_api
@@ -265,6 +266,163 @@ class JobController(BaseAPIController, UsesLibraryMixinItems):
exceptions.RequestParameterInvalidException("Job with id '%s' is not paused" % (job.tool_id))
return self.__dictify_associations(trans, job.output_datasets, job.output_library_datasets)
+ @expose_api_anonymous
+ def metrics(self, trans, **kwd):
+ """
+ * GET /api/jobs/{job_id}/metrics
+ * GET /api/datasets/{dataset_id}/metrics
+ Return job metrics for specified job. Job accessibility checks are slightly
+ different than dataset checks, so both methods are available.
+
+ :type job_id: string
+ :param job_id: Encoded job id
+
+ :type dataset_id: string
+ :param dataset_id: Encoded HDA or LDDA id
+
+ :type hda_ldda: string
+ :param hda_ldda: hda if dataset_id is an HDA id (default), ldda if
+ it is an ldda id.
+
+ :rtype: list
+ :returns: list containing job metrics
+ """
+ job = self.__get_job(trans, **kwd)
+ if not trans.user_is_admin and not trans.app.config.expose_potentially_sensitive_job_metrics:
+ return []
+
+ return self._metrics_as_dict(trans, job)
+
+ def _metrics_as_dict(self, trans, job):
+
+ def metric_to_dict(metric):
+ metric_name = metric.metric_name
+ metric_value = metric.metric_value
+ metric_plugin = metric.plugin
+ title, value = trans.app.job_metrics.format(metric_plugin, metric_name, metric_value)
+ return dict(
+ title=title,
+ value=value,
+ plugin=metric_plugin,
+ name=metric_name,
+ raw_value=str(metric_value),
+ )
+
+ metrics = [m for m in job.metrics if m.plugin != 'env' or trans.user_is_admin]
+ return list(map(metric_to_dict, metrics))
+
+ @expose_api_anonymous
+ def parameters_display(self, trans, **kwd):
+ """
+ * GET /api/jobs/{job_id}/parameters_display
+ * GET /api/datasets/{dataset_id}/parameters_display
+
+ Resolve parameters as a list for nested display. More client logic
+ here than is ideal but it is hard to reason about tool parameter
+ types on the client relative to the server. Job accessibility checks
+ are slightly different than dataset checks, so both methods are
+ available.
+
+ This API endpoint is unstable and tied heavily to Galaxy's JS client code,
+ this endpoint will change frequently.
+
+ :type job_id: string
+ :param job_id: Encoded job id
+
+ :type dataset_id: string
+ :param dataset_id: Encoded HDA or LDDA id
+
+ :type hda_ldda: string
+ :param hda_ldda: hda if dataset_id is an HDA id (default), ldda if
+ it is an ldda id.
+
+ :rtype: list
+ :returns: job parameters for for display
+ """
+ job = self.__get_job(trans, **kwd)
+
+ def inputs_recursive(input_params, param_values, depth=1, upgrade_messages=None):
+ if upgrade_messages is None:
+ upgrade_messages = {}
+
+ rval = []
+
+ for input_index, input in enumerate(input_params.values()):
+ if input.name in param_values:
+ if input.type == "repeat":
+ for i in range(len(param_values[input.name])):
+ rval.extend(inputs_recursive(input.inputs, param_values[input.name][i], depth=depth + 1))
+ elif input.type == "section":
+ # Get the value of the current Section parameter
+ rval.append(dict(text=input.name, depth=depth))
+ rval.extend(inputs_recursive(input.inputs, param_values[input.name], depth=depth + 1, upgrade_messages=upgrade_messages.get(input.name)))
+ elif input.type == "conditional":
+ try:
+ current_case = param_values[input.name]['__current_case__']
+ is_valid = True
+ except Exception:
+ current_case = None
+ is_valid = False
+ if is_valid:
+ rval.append(dict(text=input.test_param.label, depth=depth, value=input.cases[current_case].value))
+ rval.extend(inputs_recursive(input.cases[current_case].inputs, param_values[input.name], depth=depth + 1, upgrade_messages=upgrade_messages.get(input.name)))
+ else:
+ rval.append(dict(text=input.name, depth=depth, notes="The previously used value is no longer valid.", error=True))
+ elif input.type == "upload_dataset":
+ rval.append(dict(text=input.group_title(param_values), depth=depth, value="%s uploaded datasets" % len(param_values[input.name])))
+ elif input.type == "data":
+ value = []
+ for i, element in enumerate(util.listify(param_values[input.name])):
+ if element.history_content_type == "dataset":
+ hda = element
+ encoded_id = trans.security.encode_id(hda.id)
+ value.append({"src": "hda", "id": encoded_id, "hid": hda.hid, "name": hda.name})
+ else:
+ value.append({"hid": element.hid, "name": element.name})
+ rval.append(dict(text=input.label, depth=depth, value=value))
+ elif input.visible:
+ if hasattr(input, "label") and input.label:
+ label = input.label
+ else:
+ # value for label not required, fallback to input name (same as tool panel)
+ label = input.name
+ rval.append(dict(text=label, depth=depth, value=input.value_to_display_text(param_values[input.name]), notes=upgrade_messages.get(input.name, '')))
+ else:
+ # Parameter does not have a stored value.
+ # Get parameter label.
+ if input.type == "conditional":
+ label = input.test_param.label
+ elif input.type == "repeat":
+ label = input.label()
+ else:
+ label = input.label or input.name
+ rval.append(dict(text=label, depth=depth, notes="not used (parameter was added after this job was run)"))
+
+ return rval
+
+ # Load the tool
+ toolbox = self.app.toolbox
+ tool = toolbox.get_tool(job.tool_id, job.tool_version)
+ assert tool is not None, 'Requested tool has not been loaded.'
+
+ params_objects = None
+ upgrade_messages = {}
+ has_parameter_errors = False
+
+ # Load parameter objects, if a parameter type has changed, it's possible for the value to no longer be valid
+ try:
+ params_objects = job.get_param_values(self.app, ignore_errors=False)
+ except Exception:
+ params_objects = job.get_param_values(self.app, ignore_errors=True)
+ # use different param_objects in the following line, since we want to display original values as much as possible
+ upgrade_messages = tool.check_and_update_param_values(job.get_param_values(self.app, ignore_errors=True),
+ trans,
+ update_values=False)
+ has_parameter_errors = True
+
+ parameters = inputs_recursive(tool.inputs, params_objects, depth=1, upgrade_messages=upgrade_messages)
+ return {"parameters": parameters, "has_parameter_errors": has_parameter_errors}
+
@expose_api_anonymous
def build_for_rerun(self, trans, id, **kwd):
"""
@@ -306,23 +464,18 @@ class JobController(BaseAPIController, UsesLibraryMixinItems):
dataset_dict = dict(src="ldda", id=trans.security.encode_id(dataset.id))
return dict(name=job_dataset_association.name, dataset=dataset_dict)
- def __get_job(self, trans, id):
- try:
- decoded_job_id = self.decode_id(id)
- except Exception:
- raise exceptions.MalformedId()
- job = trans.sa_session.query(trans.app.model.Job).filter(trans.app.model.Job.id == decoded_job_id).first()
- if job is None:
- raise exceptions.ObjectNotFound()
- belongs_to_user = (job.user == trans.user) if job.user else (job.session_id == trans.get_galaxy_session().id)
- if not trans.user_is_admin and not belongs_to_user:
- # Check access granted via output datasets.
- if not job.output_datasets:
- raise exceptions.ItemAccessibilityException("Job has no output datasets.")
- for data_assoc in job.output_datasets:
- if not self.dataset_manager.is_accessible(data_assoc.dataset.dataset, trans.user):
- raise exceptions.ItemAccessibilityException("You are not allowed to rerun this job.")
- return job
+ def __get_job(self, trans, job_id=None, dataset_id=None, **kwd):
+ if job_id is not None:
+ try:
+ decoded_job_id = self.decode_id(job_id)
+ except Exception:
+ raise exceptions.MalformedId()
+ return self.job_manager.get_accessible_job(trans, decoded_job_id)
+ else:
+ hda_ldda = kwd.get("hda_ldda", "hda")
+ # Following checks dataset accessible
+ dataset_instance = self.get_hda_or_ldda(trans, hda_ldda=hda_ldda, dataset_id=dataset_id)
+ return dataset_instance.creating_job
@expose_api
def create(self, trans, payload, **kwd):
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index 9289ab0b1f5..8f9825ed845 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -890,6 +890,11 @@ def populate_api_routes(webapp, app):
webapp.mapper.connect('resume', '/api/jobs/{id}/resume', controller='jobs', action='resume', conditions=dict(method=['PUT']))
webapp.mapper.connect('job_error', '/api/jobs/{id}/error', controller='jobs', action='error', conditions=dict(method=['POST']))
webapp.mapper.connect('common_problems', '/api/jobs/{id}/common_problems', controller='jobs', action='common_problems', conditions=dict(method=['GET']))
+ # Job metrics and parameters by job id or dataset id (for slightly different accessibility checking)
+ webapp.mapper.connect('metrics', '/api/jobs/{job_id}/metrics', controller='jobs', action='metrics', conditions=dict(method=['GET']))
+ webapp.mapper.connect('dataset_metrics', '/api/datasets/{dataset_id}/metrics', controller='jobs', action='metrics', conditions=dict(method=['GET']))
+ webapp.mapper.connect('parameters_display', '/api/jobs/{job_id}/parameters_display', controller='jobs', action='parameters_display', conditions=dict(method=['GET']))
+ webapp.mapper.connect('dataset_parameters_display', '/api/datasets/{dataset_id}/parameters_display', controller='jobs', action='parameters_display', conditions=dict(method=['GET']))
# Job files controllers. Only for consumption by remote job runners.
webapp.mapper.resource('file',
diff --git a/templates/show_params.mako b/templates/show_params.mako
index daeeb837d84..2a4e8f3549c 100644
--- a/templates/show_params.mako
+++ b/templates/show_params.mako
@@ -9,133 +9,8 @@
text-align: center;
background-color: #eee;
}
-
- table.info_data_table {
- table-layout: fixed;
- word-break: break-word;
- }
- table.info_data_table td:nth-child(1) {
- width: 25%;
- }
-
-<%def name="inputs_recursive( input_params, param_values, depth=1, upgrade_messages=None )">
- <%
- from galaxy.util import listify
- if upgrade_messages is None:
- upgrade_messages = {}
- %>
- %for input_index, input in enumerate( input_params.values() ):
- %if input.name in param_values:
- %if input.type == "repeat":
- %for i in range( len(param_values[input.name]) ):
- ${ inputs_recursive(input.inputs, param_values[input.name][i], depth=depth+1) }
- %endfor
- %elif input.type == "section":
-
- ##
- ${inputs_recursive_indent( text=input.name, depth=depth )}
- |
-
- ${ inputs_recursive( input.inputs, param_values[input.name], depth=depth+1, upgrade_messages=upgrade_messages.get( input.name ) ) }
- %elif input.type == "conditional":
- <%
- try:
- current_case = param_values[input.name]['__current_case__']
- is_valid = True
- except:
- current_case = None
- is_valid = False
- %>
- %if is_valid:
-
- ${ inputs_recursive_indent( text=input.test_param.label, depth=depth )}
- ##
- | ${input.cases[current_case].value | h} |
- |
-
- ${ inputs_recursive( input.cases[current_case].inputs, param_values[input.name], depth=depth+1, upgrade_messages=upgrade_messages.get( input.name ) ) }
- %else:
-
- ${ inputs_recursive_indent( text=input.name, depth=depth )}
- | The previously used value is no longer valid |
- |
-
- %endif
- %elif input.type == "upload_dataset":
-
- ${inputs_recursive_indent( text=input.group_title( param_values ), depth=depth )}
- | ${ len( param_values[input.name] ) } uploaded datasets |
- |
-
- ## files used for inputs
- %elif input.type == "data":
-
- ${inputs_recursive_indent( text=input.label, depth=depth )}
- |
- %for i, element in enumerate(listify(param_values[input.name])):
- %if i > 0:
- ,
- %endif
- %if element.history_content_type == "dataset":
- <%
- hda = element
- encoded_id = trans.security.encode_id( hda.id )
- show_params_url = h.url_for( controller='dataset', action='show_params', dataset_id=encoded_id )
- %>
- ${hda.hid}: ${hda.name | h}
-
- %else:
- ${element.hid}: ${element.name | h}
- %endif
- %endfor
- |
- |
-
- %elif input.visible:
- <%
- if hasattr( input, "label" ) and input.label:
- label = input.label
- else:
- #value for label not required, fallback to input name (same as tool panel)
- label = input.name
- %>
-
- ${inputs_recursive_indent( text=label, depth=depth )}
- | ${input.value_to_display_text( param_values[input.name] ) | h} |
- ${ upgrade_messages.get( input.name, '' ) | h } |
-
- %endif
- %else:
- ## Parameter does not have a stored value.
-
- <%
- # Get parameter label.
- if input.type == "conditional":
- label = input.test_param.label
- elif input.type == "repeat":
- label = input.label()
- else:
- label = input.label or input.name
- %>
- ${inputs_recursive_indent( text=label, depth=depth )}
- | not used (parameter was added after this job was run) |
- |
-
- %endif
-
- %endfor
-%def>
-
- ## function to add a indentation depending on the depth in a
-<%def name="inputs_recursive_indent( text, depth )">
- |
- ${text | h}
- |
-%def>
-
% if tool:
${tool.name | h}
@@ -212,31 +87,13 @@
-Tool Parameters
-
-%if has_parameter_errors:
-
- ${ render_msg( 'One or more of your original parameters may no longer be valid or displayed properly.', status='warning' ) }
+%if job:
+
+
%endif
+
Inheritance Chain
${hda.name | h}
@@ -256,26 +113,8 @@ ${ job.command_line | h }
%endif
%if job and (trans.user_is_admin or trans.app.config.expose_potentially_sensitive_job_metrics):
-Job Metrics
-<% job_metrics = trans.app.job_metrics %>
-<% plugins = set([metric.plugin for metric in job.metrics]) %>
- %for plugin in sorted(plugins):
- %if trans.user_is_admin or plugin != 'env':
- ${ plugin | h }
-
-
- <%
- plugin_metrics = filter(lambda x: x.plugin == plugin, job.metrics)
- plugin_metric_displays = [job_metrics.format( metric.plugin, metric.metric_name, metric.metric_value ) for metric in plugin_metrics]
- plugin_metric_displays = sorted(plugin_metric_displays, key=lambda pair: pair[0]) # Sort on displayed title
- %>
- %for metric_title, metric_value in plugin_metric_displays:
- | ${ metric_title | h } | ${ metric_value | h } |
- %endfor
-
-
- %endif
- %endfor
+
+
%endif
%if trans.user_is_admin:
@@ -352,5 +191,7 @@ $(function(){
window.parent.Galaxy.currHistoryPanel.scrollToId( 'dataset-' + $( this ).data( 'hda-id' ) );
}
})
+ window.bundleEntries.mountJobMetrics();
+ window.bundleEntries.mountJobParameters();
});
diff --git a/test/base/populators.py b/test/base/populators.py
index e06b8d23f0f..0ee8b19d416 100644
--- a/test/base/populators.py
+++ b/test/base/populators.py
@@ -1372,7 +1372,7 @@ def load_data_dict(history_id, test_data, dataset_populator, dataset_collection_
def wait_on_state(state_func, desc="state", skip_states=["running", "queued", "new", "ready"], assert_ok=False, timeout=DEFAULT_TIMEOUT):
def get_state():
response = state_func()
- assert response.status_code == 200, "Failed to fetch state update while waiting."
+ assert response.status_code == 200, "Failed to fetch state update while waiting. [%s]" % response.content
state = response.json()["state"]
if state in skip_states:
return None