Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User-facing objectstore metadata. #10233

Merged
merged 4 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/src/bundleEntries.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export { mountPageEditor } from "components/PageEditor/mount";
export { mountPageDisplay } from "components/PageDisplay";
export { mountDestinationParams } from "components/JobDestinationParams";
export { mountDatasetInformation } from "components/DatasetInformation";
export { mountDatasetStorage } from "components/Dataset/DatasetStorage";

// Used in common.mako
export { default as store } from "storemodern";
72 changes: 72 additions & 0 deletions client/src/components/Dataset/DatasetStorage/DatasetStorage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div>
<h3 v-if="includeTitle">Dataset Storage</h3>
<loading-span v-if="storageInfo == null"> </loading-span>
<div v-else>
<p v-if="storageInfo.name">
This data is stored in a Galaxy ObjectStore named <b>{{ storageInfo.name }}</b
>.
</p>
<p v-else-if="storageInfo.object_store_id">
This data is stored in a Galaxy ObjectStore with id <b>{{ storageInfo.object_store_id }}</b
>.
</p>
<p v-else>
This data is stored in the default configured Galaxy ObjectStore.
</p>
<div v-html="descriptionRendered"></div>
</div>
</div>
</template>

<script>
import axios from "axios";
import { getAppRoot } from "onload/loadConfig";
import LoadingSpan from "components/LoadingSpan";
import MarkdownIt from "markdown-it";

const md = MarkdownIt();

export default {
components: {
LoadingSpan,
},
props: {
datasetId: {
type: String,
},
datasetType: {
type: String,
default: "hda",
},
includeTitle: {
type: Boolean,
default: true,
},
},
data() {
return {
storageInfo: null,
descriptionRendered: null,
};
},
created() {
const datasetId = this.datasetId;
const datasetType = this.datasetType;
axios
.get(`${getAppRoot()}api/datasets/${datasetId}/storage?hda_ldda=${datasetType}`)
.then(this.handleResponse)
.catch(this.handleError);
},
methods: {
handleResponse(response) {
console.log(response);
this.storageInfo = response.data;
this.descriptionRendered = md.render(this.storageInfo.description);
},
handleError(err) {
console.log(err);
},
},
};
</script>
4 changes: 4 additions & 0 deletions client/src/components/Dataset/DatasetStorage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as DatasetStorage } from "./DatasetStorage";

// functions for mounting job metrics in non-Vue environments
export { mountDatasetStorage } from "./mount";
16 changes: 16 additions & 0 deletions client/src/components/Dataset/DatasetStorage/mount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Endpoint for mounting job metrics from non-Vue environment.
*/
import DatasetStorage from "./DatasetStorage.vue";
import { mountVueComponent } from "utils/mountVueComponent";

export const mountDatasetStorage = (propsData = {}) => {
const elements = document.querySelectorAll(".dataset-storage");
Array.prototype.forEach.call(elements, function (el, i) {
const datasetId = el.getAttribute("dataset_id");
const datasetType = el.getAttribute("dataset_type") || "hda";
propsData.datasetId = datasetId;
propsData.datasetType = datasetType;
mountVueComponent(DatasetStorage)(propsData, el);
});
};
6 changes: 3 additions & 3 deletions client/src/components/JobDestinationParams/mount.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* Endpoint for mounting job metrics from non-Vue environment.
*/
import $ from "jquery";
import JobDestinationParams from "./JobDestinationParams.vue";
import { mountVueComponent } from "utils/mountVueComponent";

export const mountDestinationParams = (propsData = {}) => {
$(".job-destination-parameters").each((index, el) => {
propsData.jobId = $(el).attr("job_id");
const elements = document.querySelectorAll(".job-destination-parameters");
Array.prototype.forEach.call(elements, function (el, i) {
propsData.jobId = el.getAttribute("job_id");
mountVueComponent(JobDestinationParams)(propsData, el);
});
};
12 changes: 6 additions & 6 deletions client/src/components/JobMetrics/mount.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/**
* Endpoint for mounting job metrics from non-Vue environment.
*/
import $ from "jquery";
import JobMetrics from "./JobMetrics.vue";
import { mountVueComponent } from "utils/mountVueComponent";

export const mountJobMetrics = (propsData = {}) => {
$(".job-metrics").each((index, el) => {
const jobId = $(el).attr("job_id");
const aws_estimate = $(el).attr("aws_estimate");
const datasetId = $(el).attr("dataset_id");
const datasetType = $(el).attr("dataset_type") || "hda";
const elements = document.querySelectorAll(".job-metrics");
Array.prototype.forEach.call(elements, function (el, i) {
const jobId = el.getAttribute("job_id");
const aws_estimate = el.getAttribute("aws_estimate");
const datasetId = el.getAttribute("dataset_id");
const datasetType = el.getAttribute("dataset_type") || "hda";
propsData.jobId = jobId;
propsData.datasetId = datasetId;
propsData.datasetType = datasetType;
Expand Down
17 changes: 8 additions & 9 deletions client/src/components/JobParameters/mount.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/**
* Endpoint for mounting job parameters from non-Vue environment.
*/
import $ from "jquery";
import Vue from "vue";
import JobParameters from "./JobParameters.vue";
import { mountVueComponent } from "utils/mountVueComponent";

export const mountJobParameters = (propsData = {}) => {
$(".job-parameters").each((index, el) => {
const jobId = $(el).attr("job_id");
const datasetId = $(el).attr("dataset_id");
const param = $(el).attr("param");
const datasetType = $(el).attr("dataset_type") || "hda";
const component = Vue.extend(JobParameters);
const elements = document.querySelectorAll(".job-parameters");
Array.prototype.forEach.call(elements, function (el, i) {
const jobId = el.getAttribute("job_id");
const datasetId = el.getAttribute("dataset_id");
const param = el.getAttribute("param") || undefined;
const datasetType = el.getAttribute("dataset_type") || "hda";
propsData.jobId = jobId;
propsData.datasetId = datasetId;
propsData.param = param;
propsData.datasetType = datasetType;
return new component({ propsData: propsData }).$mount(el);
mountVueComponent(JobParameters)(propsData, el);
});
};
45 changes: 44 additions & 1 deletion lib/galaxy/objectstore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,28 @@ def update_from_file(self, obj, base_dir=None, extra_dir=None, extra_dir_at_root
@abc.abstractmethod
def get_object_url(self, obj, extra_dir=None, extra_dir_at_root=False, alt_name=None, obj_dir=False):
"""
Return the URL for direct acces if supported, otherwise return None.
Return the URL for direct access if supported, otherwise return None.

Note: need to be careful to not bypass dataset security with this.
"""
raise NotImplementedError()

@abc.abstractmethod
def get_concrete_store_name(self, obj):
"""Return a display name or title of the objectstore corresponding to obj.

To accommodate nested objectstores, obj is passed in so this metadata can
be returned for the ConcreteObjectStore corresponding to the object.
"""

@abc.abstractmethod
def get_concrete_store_description_markdown(self, obj):
"""Return a longer description of how data 'obj' is stored.

To accommodate nested objectstores, obj is passed in so this metadata can
be returned for the ConcreteObjectStore corresponding to the object.
"""

@abc.abstractmethod
def get_store_usage_percent(self):
"""Return the percentage indicating how full the store is."""
Expand Down Expand Up @@ -292,6 +308,12 @@ def update_from_file(self, obj, **kwargs):
def get_object_url(self, obj, **kwargs):
return self._invoke('get_object_url', obj, **kwargs)

def get_concrete_store_name(self, obj):
return self._invoke('get_concrete_store_name', obj)

def get_concrete_store_description_markdown(self, obj):
return self._invoke('get_concrete_store_description_markdown', obj)

def get_store_usage_percent(self):
return self._invoke('get_store_usage_percent')

Expand Down Expand Up @@ -323,12 +345,22 @@ def __init__(self, config, config_dict=None, **kwargs):
config_dict = {}
super().__init__(config=config, config_dict=config_dict, **kwargs)
self.store_by = config_dict.get("store_by", None) or getattr(config, "object_store_store_by", "id")
self.name = config_dict.get("name", None)
self.description = config_dict.get("description", None)

def to_dict(self):
rval = super().to_dict()
rval["store_by"] = self.store_by
rval["name"] = self.name
rval["description"] = self.description
return rval

def _get_concrete_store_name(self, obj):
return self.name

def _get_concrete_store_description_markdown(self, obj):
return self.description

def _get_store_by(self, obj):
return self.store_by

Expand Down Expand Up @@ -378,9 +410,14 @@ def parse_xml(clazz, config_xml):
store_by = config_xml.attrib.get('store_by', None)
if store_by is not None:
config_dict['store_by'] = store_by
name = config_xml.attrib.get('name', None)
if name is not None:
config_dict['name'] = name
for e in config_xml:
if e.tag == 'files_dir':
config_dict["files_dir"] = e.get('path')
elif e.tag == 'description':
config_dict["description"] = e.text
else:
extra_dirs.append({"type": e.get('type'), "path": e.get('path')})

Expand Down Expand Up @@ -659,6 +696,12 @@ def _get_object_url(self, obj, **kwargs):
"""For the first backend that has this `obj`, get its URL."""
return self._call_method('_get_object_url', obj, None, False, **kwargs)

def _get_concrete_store_name(self, obj):
return self._call_method('_get_concrete_store_name', obj, None, True)

def _get_concrete_store_description_markdown(self, obj):
return self._call_method('_get_concrete_store_description_markdown', obj, None, True)

def _get_store_by(self, obj):
return self._call_method('_get_store_by', obj, None, False)

Expand Down
28 changes: 28 additions & 0 deletions lib/galaxy/webapps/galaxy/api/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,34 @@ def show(self, trans, id, hda_ldda='hda', data_type=None, provider=None, **kwd):
rval = dataset.to_dict()
return rval

@web.expose_api_anonymous
def show_storage(self, trans, dataset_id, hda_ldda='hda', **kwd):
"""
GET /api/datasets/{encoded_dataset_id}/storage

Display user-facing storage details related to the objectstore a
dataset resides in.
"""
dataset_instance = self.get_hda_or_ldda(trans, hda_ldda=hda_ldda, dataset_id=dataset_id)
dataset = dataset_instance.dataset
object_store = self.app.object_store
object_store_id = dataset.object_store_id
name = object_store.get_concrete_store_name(dataset)
description = object_store.get_concrete_store_description_markdown(dataset)
# not really working (existing problem)
try:
percent_used = object_store.get_store_usage_percent()
except AttributeError:
# not implemented on nestedobjectstores yet.
percent_used = None

return {
'object_store_id': object_store_id,
'name': name,
'description': description,
'percent_used': percent_used,
}

@web.expose_api
def update_permissions(self, trans, dataset_id, payload, **kwd):
"""
Expand Down
5 changes: 5 additions & 0 deletions lib/galaxy/webapps/galaxy/buildapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ def populate_api_routes(webapp, app):
controller="datasets",
action="get_content_as_text",
conditions=dict(method=["GET"]))
webapp.mapper.connect('dataset_storage',
'/api/datasets/{dataset_id}/storage',
controller='datasets',
action='show_storage',
conditions=dict(method=["GET"]))
webapp.mapper.connect("history_contents_metadata_file",
"/api/histories/{history_id}/contents/{history_content_id}/metadata_file",
controller="datasets",
Expand Down
5 changes: 5 additions & 0 deletions lib/galaxy_test/base/populators.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,11 @@ def ds_entry(self, history_content):
src = 'hdca'
return dict(src=src, id=history_content["id"])

def dataset_storage_info(self, dataset_id):
storage_response = self.galaxy_interactor.get("datasets/{}/storage".format(dataset_id))
storage_response.raise_for_status()
return storage_response.json()

def get_roles(self):
roles_response = self.galaxy_interactor.get("roles", admin=True)
assert roles_response.status_code == 200
Expand Down
4 changes: 4 additions & 0 deletions templates/show_params.mako
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ encoded_history_id = trans.security.encode_id( hda.history_id )
</tbody>
</table>

<div class="dataset-storage" dataset_id="${encoded_hda_id}" dataset_type="hda">
</div>

%if job:
<div class="job-parameters" dataset_id="${encoded_hda_id}" dataset_type="hda">
</div>
Expand Down Expand Up @@ -175,5 +178,6 @@ $(function(){
window.bundleEntries.mountJobParameters();
window.bundleEntries.mountDestinationParams();
window.bundleEntries.mountDatasetInformation();
window.bundleEntries.mountDatasetStorage();
});
</script>
6 changes: 5 additions & 1 deletion test/integration/objectstore/test_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
DISTRIBUTED_OBJECT_STORE_CONFIG_TEMPLATE = string.Template("""<?xml version="1.0"?>
<object_store type="distributed" id="primary" order="0">
<backends>
<backend id="default" type="disk" weight="1">
<backend id="default" type="disk" weight="1" name="Default Store">
<description>This is my description of the default store with *markdown*.</description>
<files_dir path="${temp_directory}/files_default"/>
<extra_dir type="temp" path="${temp_directory}/tmp_default"/>
<extra_dir type="job_work" path="${temp_directory}/job_working_directory_default"/>
Expand Down Expand Up @@ -86,6 +87,9 @@ def _run_tool(tool_id, inputs):
hda1 = self.dataset_populator.new_dataset(history_id, content="1 2 3")
self.dataset_populator.wait_for_history(history_id)
hda1_input = {"src": "hda", "id": hda1["id"]}
storage_info = self.dataset_populator.dataset_storage_info(hda1["id"])
assert "Default Store" == storage_info["name"]
assert "*markdown*" in storage_info["description"]

# One file uploaded, added to default object store ID.
self._assert_file_counts(1, 0, 0, 0)
Expand Down
Loading