Merge branch 'release_22.05' into dev

This commit is contained in:
Nicola Soranzo
2022-06-27 10:55:24 +01:00
20 changed files with 236 additions and 58 deletions
@@ -4,7 +4,7 @@
:options-show="optionsShow"
:modal-show="modalShow"
:hide-modal="() => (modalShow = false)"
:back-func="load"
:back-func="goBack"
:undo-show="undoShow">
<template v-slot:search>
<data-dialog-search v-model="filter" />
@@ -330,6 +330,10 @@ export default {
});
}
},
goBack() {
// Loading without a record navigates back one level
this.load();
},
parseItemFileMode(item) {
const result = {
id: item.uri,
@@ -11,7 +11,7 @@ export const STATES = {
},
deferred: {
status: "info",
text: "This dataset is remote, has not be ingested by Galaxy, and full metadata may not be available.",
text: "This dataset is remote, has not been ingested by Galaxy, and full metadata may not be available.",
icon: "cloud",
},
/** has no data */
@@ -130,7 +130,7 @@
<SelectorModal
id="selector-history-modal"
:histories="histories"
:current-history="history"
:current-history-id="history.id"
@selectHistory="$emit('setCurrentHistory', $event)" />
<CopyModal id="copy-history-modal" :history="history" />
@@ -0,0 +1,65 @@
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { getLocalVue } from "jest/helpers";
import SelectorModal from "./SelectorModal";
const localVue = getLocalVue();
const SELECTED_HISTORY_ID = "COOL_ID";
const getFakeHistorySummaries = (num, selectedIndex = 0) => {
const result = Array.from({ length: num }, (_, index) => ({
id: `ID-${index}`,
name: `History-${index}`,
tags: [],
update_time: new Date().toISOString(),
}));
result[selectedIndex].id = SELECTED_HISTORY_ID;
return result;
};
const PROPS_WITH_10_HISTORIES = {
currentHistoryId: SELECTED_HISTORY_ID,
histories: getFakeHistorySummaries(10),
perPage: 3,
static: true, // Force the modal visible for testing
};
describe("History SelectorModal.vue", () => {
let wrapper;
async function mountWith(props) {
wrapper = mount(SelectorModal, {
propsData: props,
localVue,
});
await flushPromises();
}
it("should highlight the currently selected history", async () => {
await mountWith(PROPS_WITH_10_HISTORIES);
const selectedRows = wrapper.findAll(".table-success");
expect(selectedRows.length).toBe(1);
expect(selectedRows.at(0).attributes("data-pk")).toBe(SELECTED_HISTORY_ID);
});
it("paginates the histories", async () => {
await mountWith(PROPS_WITH_10_HISTORIES);
const displayedRows = wrapper.findAll("tbody > tr").wrappers;
expect(displayedRows.length).toBe(3);
expect(wrapper.vm.histories.length).toBe(10);
});
it("emits selectHistory with the correct history ID when a row is clicked", async () => {
await mountWith(PROPS_WITH_10_HISTORIES);
expect(wrapper.emitted()["selectHistory"]).toBeUndefined();
const targetHistoryId = "ID-2";
const targetRow = wrapper.find(`[data-pk="${targetHistoryId}"]`);
await targetRow.trigger("click");
expect(wrapper.emitted()["selectHistory"]).toBeDefined();
expect(wrapper.emitted()["selectHistory"][0][0].id).toBe(targetHistoryId);
});
});
@@ -1,25 +1,24 @@
<template>
<b-modal ref="modal" v-bind="$attrs" :title="'Switch to History' | l" v-on="$listeners">
<b-form-group :description="'Filter histories' | l">
<b-input v-model="filter" :placeholder="'Search Filter' | l" />
<b-form-input v-model="filter" type="search" :placeholder="'Search Filter' | l" />
</b-form-group>
<b-table
ref="history-list"
v-model="currentRows"
striped
hover
sticky-header="50vh"
primary-key="id"
:fields="fields"
:filter="filter"
:items="histories"
:items="formattedItems"
:per-page="perPage"
:current-page="currentPage"
:selectable="true"
select-mode="single"
selected-variant="success"
@row-selected="switchToHistory">
@row-selected="switchToHistory"
@filtered="onFiltered">
<template v-slot:cell(tags)="row">
<stateless-tags :value="row.item.tags" :disabled="true" />
</template>
@@ -29,23 +28,28 @@
</b-table>
<template v-slot:modal-footer>
<b-pagination v-model="currentPage" :total-rows="filteredRowCount" :per-page="perPage"></b-pagination>
<b-pagination v-model="currentPage" :total-rows="totalRows" :per-page="perPage" />
</template>
</b-modal>
</template>
<script>
import { BModal, BFormGroup, BFormInput, BTable, BPagination } from "bootstrap-vue";
import { StatelessTags } from "components/Tags";
import UtcDate from "components/UtcDate";
import { debounce } from "underscore";
export default {
components: {
StatelessTags,
UtcDate,
BModal,
BFormGroup,
BFormInput,
BTable,
BPagination,
},
props: {
currentHistory: { type: Object, required: true },
currentHistoryId: { type: String, required: true },
histories: { type: Array, default: () => [] },
perPage: { type: Number, required: false, default: 50 },
},
@@ -53,30 +57,22 @@ export default {
return {
filter: null,
currentPage: 1,
currentRows: [],
totalRows: 0,
};
},
computed: {
filteredRowCount() {
return this.currentRows.length;
},
selectedIndex() {
return this.currentRows.findIndex((h) => h.id == this.currentHistory.id);
formattedItems() {
return this.histories.map((item) => {
if (item.id == this.currentHistoryId) {
item._rowVariant = "success";
}
return item;
});
},
},
watch: {
currentRows() {
this.selectCurrentRow();
},
filteredRowCount(newVal, oldVal) {
if (newVal != oldVal) {
this.currentPage = 1;
}
},
selectedIndex(idx, oldIdx) {
if (idx != oldIdx) {
this.debounceSelectCurrentRow();
}
histories(newVal) {
this.totalRows = newVal.length;
},
},
created() {
@@ -85,7 +81,6 @@ export default {
{ key: "tags", sortable: true },
{ key: "update_time", label: "Updated", sortable: true },
];
this.debounceSelectCurrentRow = debounce(this.selectCurrentRow, 100);
},
methods: {
switchToHistory(selected) {
@@ -93,12 +88,9 @@ export default {
this.$emit("selectHistory", selected[0]);
}
},
selectCurrentRow() {
const idx = this.selectedIndex;
const list = this.$refs["history-list"];
if (list && idx > -1) {
list.selectRow(idx);
}
onFiltered(filteredItems) {
this.totalRows = filteredItems.length;
this.currentPage = 1;
},
},
};
+8 -2
View File
@@ -65,7 +65,7 @@
<span class="fa fa-star-o" />
</b-button>
<b-button
v-else
v-else-if="showRemoveFavorite"
v-b-tooltip.hover
role="button"
title="Remove from Favorites"
@@ -176,8 +176,14 @@ export default {
versions() {
return this.options.versions;
},
hasUser() {
return !this.user.isAnonymous;
},
showAddFavorite() {
return !!this.user.email && !this.isFavorite;
return this.hasUser && !this.isFavorite;
},
showRemoveFavorite() {
return this.hasUser && !this.showAddFavorite;
},
showVersions() {
return this.versions && this.versions.length > 1;
+3 -7
View File
@@ -14,9 +14,7 @@ Do you use tools which require colour inputs, like Circos? Previously we had a r
.. raw:: html
<video controls autoplay loop muted width="100%" alt="Gif of the colour selector in action, a mouse clicks on the colour selector and gone is the restricted 20 colour palette, now there is a colour field and you can select any possible RGB colour.">
<source src="../_static/22.01-colour.mp4" type="video/mp4">
</video>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/DSnFa5012RE" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen alt="Gif of the colour selector in action, a mouse clicks on the colour selector and gone is the restricted 20 colour palette, now there is a colour field and you can select any possible RGB colour."></iframe>
**Improved File Export**
@@ -25,9 +23,7 @@ If you've been exporting files from Galaxy lately, you've probably seen the amaz
.. raw:: html
<video controls autoplay loop muted width="100%" alt="Video of a user exporting a file using the new file export, selecting a folder, creating some subdirectories, before changing their mind and choosing another location to export their file to.">
<source src="../_static/22.01-export.mp4" type="video/mp4">
</video>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/KBFrgWGlPus" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen alt="Video of a user exporting a file using the new file export, selecting a folder, creating some subdirectories, before changing their mind and choosing another location to export their file to."></iframe>
**Improved File Uploads**
@@ -142,7 +138,7 @@ A special thanks to the release testing team for testing many of the new feature
- `Natalie Kucher <https://github.com/nakucher>`_
- `Alex Ostrovsky <https://github.com/astrovsky01>`_
- `Enis Afgan <https://github.com/afgane>`_
Release Notes
===========================================================
Binary file not shown.
Binary file not shown.
+4 -2
View File
@@ -482,9 +482,11 @@ class HistorySerializer(sharable.SharableModelSerializer, deletable.PurgableSeri
"size": lambda item, key, **context: int(item.disk_size),
"nice_size": lambda item, key, **context: item.disk_nice_size,
"state": self.serialize_history_state,
"url": lambda item, key, **context: self.url_for("history", id=self.app.security.encode_id(item.id)),
"url": lambda item, key, **context: self.url_for(
"history", id=self.app.security.encode_id(item.id), context=context
),
"contents_url": lambda item, key, **context: self.url_for(
"history_contents", history_id=self.app.security.encode_id(item.id)
"history_contents", history_id=self.app.security.encode_id(item.id), context=context
),
"empty": lambda item, key, **context: (len(item.datasets) + len(item.dataset_collections)) <= 0,
"count": lambda item, key, **context: len(item.datasets),
+2 -2
View File
@@ -434,7 +434,7 @@ def set_metadata_portable(
if not object_store or not export_store:
# Can't happen, but type system doesn't know
raise Exception("object_store not built")
if not link_data_only and os.path.getsize(external_filename):
if not is_deferred and not link_data_only and os.path.getsize(external_filename):
# Here we might be updating a disk based objectstore when outputs_to_working_directory is used,
# or a remote object store from its cache path.
object_store.update_from_file(dataset.dataset, file_name=external_filename, create=True)
@@ -469,7 +469,7 @@ def set_metadata_portable(
context_value = context[context_key]
setattr(dataset, context_key, context_value)
# We only want to persist the external_filename if the dataset has been linked in.
if not link_data_only:
if not is_deferred and not link_data_only:
dataset.dataset.external_filename = None
dataset.dataset.extra_files_path = None
export_store.add_dataset(dataset)
+10 -6
View File
@@ -40,6 +40,7 @@ log = logging.getLogger(__name__)
class TransientDatasetPaths(NamedTuple):
external_filename: str
external_extra_files_path: str
metadata_files_dir: str
class TransientPathMapper:
@@ -61,7 +62,7 @@ class SimpleTransientPathMapper(TransientPathMapper):
external_filename = os.path.join(self._staging_directory, external_filename_basename)
external_extras_basename = "dataset_%s_files" % str(old_dataset.uuid)
external_extras = os.path.join(self._staging_directory, external_extras_basename)
return TransientDatasetPaths(external_filename, external_extras)
return TransientDatasetPaths(external_filename, external_extras, self._staging_directory)
class DatasetInstanceMaterializer:
@@ -110,6 +111,7 @@ class DatasetInstanceMaterializer:
materialized_dataset.hashes = [h.copy() for h in dataset.hashes]
target_source = self._find_closest_dataset_source(dataset)
transient_paths = None
if attached:
object_store_populator = self._object_store_populator
assert object_store_populator
@@ -157,12 +159,14 @@ class DatasetInstanceMaterializer:
materialized_dataset_instance.has_metadata_files or materialized_dataset_instance.metadata_deferred
)
if require_metadata_regeneration:
if attached and self._sa_session:
# as of mid April 2022, we now get JSON encoding errors if this
# isn't bound to the session before metadata generation.
self._sa_session.add(materialized_dataset_instance)
materialized_dataset_instance.init_meta()
materialized_dataset_instance.set_meta()
if transient_paths:
metadata_tmp_files_dir = transient_paths.metadata_files_dir
else:
# If metadata_tmp_files_dir is set we generate a MetdataTempFile,
# which we don't want when we're generating an attached materialized dataset instance
metadata_tmp_files_dir = None
materialized_dataset_instance.set_meta(metadata_tmp_files_dir=metadata_tmp_files_dir)
materialized_dataset_instance.metadata_deferred = False
return materialized_dataset_instance
@@ -180,6 +180,7 @@ class FastAPIHistories:
@router.get(
"/api/histories/{id}",
name="history",
summary="Returns the history with the given ID.",
)
def show(
@@ -713,6 +713,12 @@ class FastAPIHistoryContents:
description="When deleting a dataset collection, whether to also delete containing datasets.",
deprecated=True,
),
stop_job: Optional[bool] = Query(
default=False,
title="Stop Job",
description="Whether to stop the creating job if all outputs of the job have been deleted.",
deprecated=True,
),
payload: DeleteHistoryContentPayload = Body(None),
):
"""
@@ -725,6 +731,7 @@ class FastAPIHistoryContents:
payload = DeleteHistoryContentPayload()
payload.purge = payload.purge or purge is True
payload.recursive = payload.recursive or recursive is True
payload.stop_job = payload.stop_job or stop_job is True
rval = self.service.delete(
trans,
id=id,
+10 -1
View File
@@ -338,6 +338,13 @@ class DatasetsApiTestCase(ApiTestCase):
@skip_without_tool("cat_data_and_sleep")
def test_delete_cancels_job(self):
self._run_cancel_job(use_query_params=False)
@skip_without_tool("cat_data_and_sleep")
def test_delete_cancels_job_with_query_params(self):
self._run_cancel_job(use_query_params=True)
def _run_cancel_job(self, use_query_params=False):
hda_id = self.dataset_populator.new_dataset(self.history_id)["id"]
inputs = {
"input1": {"src": "hda", "id": hda_id},
@@ -355,7 +362,9 @@ class DatasetsApiTestCase(ApiTestCase):
assert job_details["state"] in ("new", "queued", "running"), job_details
# Use stop_job to cancel the creating job
delete_response = self.dataset_populator.delete_dataset(self.history_id, output_hda_id, stop_job=True)
delete_response = self.dataset_populator.delete_dataset(
self.history_id, output_hda_id, stop_job=True, use_query_params=use_query_params
)
self._assert_status_code_is_ok(delete_response)
deleted_hda = delete_response.json()
assert deleted_hda["deleted"], deleted_hda
+9
View File
@@ -93,6 +93,15 @@ class HistoriesApiTestCase(ApiTestCase, BaseHistories):
self._assert_has_keys(state_details, *states)
self._assert_has_keys(state_ids, *states)
def test_show_history_returns_expected_urls(self):
# This test can be dropped when the URL attributes become deprecated
history_id = self._create_history("TestHistoryForUrls")["id"]
show_response = self._show(history_id)
self._assert_has_key(show_response, "id", "url", "contents_url")
assert show_response["url"] == f"/api/histories/{history_id}"
assert show_response["contents_url"] == f"/api/histories/{history_id}/contents"
def test_show_most_recently_used(self):
history_id = self._create_history("TestHistoryRecent")["id"]
show_response = self._get("histories/most_recently_used").json()
+48
View File
@@ -2729,6 +2729,54 @@ class ToolsTestCase(ApiTestCase, TestsTools):
output_content = self.dataset_populator.get_history_dataset_content(history_id, dataset=output)
assert output_content.startswith("chr1 147962192 147962580 CCDS989.1_cds_0_0_chr1_147962193_r 0 -")
@skip_without_tool("metadata_bam")
@uses_test_history(require_new=False)
def test_run_deferred_dataset_with_metadata_options_filter(self, history_id):
details = self.dataset_populator.create_deferred_hda(
history_id, "https://raw.githubusercontent.com/galaxyproject/galaxy/dev/test-data/1.bam", ext="bam"
)
inputs = {"input_bam": dataset_to_param(details), "ref_names": "chrM"}
run_response = self.dataset_populator.run_tool(tool_id="metadata_bam", inputs=inputs, history_id=history_id)
output = run_response["outputs"][0]
output_details = self.dataset_populator.get_history_dataset_details(
history_id, dataset=output, wait=True, assert_ok=True
)
assert output_details["state"] == "ok"
output_content = self.dataset_populator.get_history_dataset_content(history_id, dataset=output)
assert output_content.startswith("chrM")
@skip_without_tool("pileup")
@uses_test_history(require_new=False)
def test_metadata_validator_on_deferred_input(self, history_id):
deferred_bam_details = self.dataset_populator.create_deferred_hda(
history_id, "https://raw.githubusercontent.com/galaxyproject/galaxy/dev/test-data/1.bam", ext="bam"
)
fasta1_contents = open(self.get_filename("1.fasta")).read()
fasta = self.dataset_populator.new_dataset(history_id, content=fasta1_contents)
inputs = {"input1": dataset_to_param(deferred_bam_details), "reference": dataset_to_param(fasta)}
run_response = self.dataset_populator.run_tool(tool_id="pileup", inputs=inputs, history_id=history_id)
self.dataset_populator.wait_for_job(run_response["jobs"][0]["id"], assert_ok=True)
@pytest.mark.xfail
@skip_without_tool("pileup")
@uses_test_history(require_new=False)
def test_metadata_validator_can_fail_on_deferred_input(self, history_id):
# This test fails because we just skip the validator
# Fixing this is a TODO
deferred_bam_details = self.dataset_populator.create_deferred_hda(
history_id,
"https://github.com/galaxyproject/galaxy/blob/dev/test-data/3unsorted.bam?raw=true",
ext="unsorted.bam",
)
fasta1_contents = open(self.get_filename("1.fasta")).read()
fasta = self.dataset_populator.new_dataset(history_id, content=fasta1_contents)
inputs = {"input1": dataset_to_param(deferred_bam_details), "reference": dataset_to_param(fasta)}
run_response = self.dataset_populator.run_tool(tool_id="pileup", inputs=inputs, history_id=history_id)
self.dataset_populator.wait_for_job(run_response["jobs"][0]["id"], assert_ok=False)
job_id = run_response["jobs"][0]["id"]
job_details = self.dataset_populator.get_job_details(job_id=job_id).json()
assert job_details["state"] == "failed"
@skip_without_tool("cat1")
@uses_test_history(require_new=False)
def test_run_deferred_mapping(self, history_id: str):
+5 -1
View File
@@ -597,9 +597,13 @@ class BaseDatasetPopulator(BasePopulator):
purge: bool = False,
stop_job: bool = False,
wait_for_purge: bool = False,
use_query_params: bool = False,
) -> Response:
dataset_url = f"histories/{history_id}/contents/{content_id}"
delete_response = self._delete(dataset_url, {"purge": purge, "stop_job": stop_job}, json=True)
if use_query_params:
delete_response = self._delete(f"{dataset_url}?purge={purge}&stop_job={stop_job}")
else:
delete_response = self._delete(dataset_url, {"purge": purge, "stop_job": stop_job}, json=True)
delete_response.raise_for_status()
if wait_for_purge and delete_response.status_code == 202:
return self.wait_for_purge(history_id, content_id)
@@ -3,7 +3,7 @@
printf 'Summary' > '$out_file1'
]]></command>
<inputs>
<param name="input1" type="data" format="bam" multiple="true" min="1" label="BAM Inputs">
<param name="input1" type="data" format="unsorted.bam" multiple="true" min="1" label="BAM Inputs">
<validator check="bam_index" message="Metadata missing, click the pencil icon in the history item and use the auto-detect feature to correct this issue." type="metadata" />
</param>
<param name="reference" type="data" format="fasta" label="Fasta Input"/>
@@ -91,6 +91,37 @@ class ExtendedMetadataIntegrationTestCase(integration_util.IntegrationTestCase):
assert dataset["created_from_basename"] == "4.bed"
class ExtendedMetadataDeferredIntegrationTestCase(integration_util.IntegrationTestCase):
def setUp(self):
super().setUp()
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
@classmethod
def handle_galaxy_config_kwds(cls, config):
config["metadata_strategy"] = "extended"
config["object_store_store_by"] = "uuid"
config["retry_metadata_internally"] = False
def test_deferred_upload_with_metadata_files(self):
history_id = self.dataset_populator.new_history()
deferred_dataset = self.dataset_populator.create_deferred_hda(
history_id=history_id,
uri="https://raw.githubusercontent.com/galaxyproject/galaxy/dev/test-data/1.bam",
ext="bam",
)
inputs = {"input1": {"src": "hda", "id": deferred_dataset["id"]}}
run_response = self.dataset_populator.run_tool("cat1", inputs=inputs, history_id=history_id)
self.dataset_populator.wait_for_job(run_response["jobs"][0]["id"], assert_ok=True)
bam_dataset = self.dataset_populator.get_history_dataset_details(
history_id=history_id, content_id=run_response["outputs"][0]["id"]
)
assert bam_dataset["state"] == "ok"
assert bam_dataset["extension"] == "bam"
metadata_response = self._get(f"datasets/{bam_dataset['id']}/metadata_file?metadata_file=bam_index")
metadata_response.raise_for_status()
assert metadata_response.content.startswith(b"BAI")
class ExtendedMetadataIntegrationInstance(integration_util.IntegrationInstance):
"""Describe a Galaxy test instance with embedded pulsar configured."""