From b84f94590974b90a6ef3c49fc9d368dcbd1c6529 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 14 Mar 2024 15:19:24 -0500 Subject: [PATCH 01/17] [23.1] Handle missing indexer for a dataset In case a dataset does not have an indexer, raise an exception. Fixes the bug mentioned in https://github.com/galaxyproject/galaxy/pull/17639#issuecomment-1995990448 --- lib/galaxy/webapps/galaxy/services/datasets.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/services/datasets.py b/lib/galaxy/webapps/galaxy/services/datasets.py index 0d1667a505d..d08fb1a7f33 100644 --- a/lib/galaxy/webapps/galaxy/services/datasets.py +++ b/lib/galaxy/webapps/galaxy/services/datasets.py @@ -868,7 +868,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): # Coverage mode uses index data. if mode == "Coverage": # Get summary using minimal cutoffs. - indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index") + indexer = self._get_indexer(trans, dataset) return indexer.get_data(chrom, low, high, **kwargs) # TODO: @@ -878,7 +878,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): # If mode is Auto, need to determine what type of data to return. if mode == "Auto": # Get stats from indexer. - indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index") + indexer = self._get_indexer(trans, dataset) stats = indexer.get_data(chrom, low, high, stats=True) # If stats were requested, return them. @@ -929,7 +929,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): # Get mean depth. if not indexer: - indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index") + indexer = self._get_indexer(trans, dataset) stats = indexer.get_data(chrom, low, high, stats=True) mean_depth = stats["data"]["mean"] @@ -981,3 +981,12 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): data = data_provider.get_data(**kwargs) return data + + def _get_indexer(self, trans, dataset): + try: + indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index") + if indexer is None: + raise Exception("No indexer available for this dataset") + return indexer + except Exception as e: + raise galaxy_exceptions.ObjectNotFound(f"Error getting indexer: {util.unicodify(e)}") From 552fe077cc1522d0dbb268385031e79e4aea36f6 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:21:51 +0100 Subject: [PATCH 02/17] Replace deprecated `Model.construct` --- lib/galaxy/webapps/galaxy/api/histories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index c8b23b35c78..410a62410dd 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -191,7 +191,7 @@ class FastAPIHistories: trans, serialization_params, filter_query_params, deleted_only=deleted, all_histories=all ) else: - payload = HistoryIndexQueryPayload.construct( + payload = HistoryIndexQueryPayload.model_construct( show_own=show_own, show_published=show_published, show_shared=show_shared, From 4fb3cbcd0e0c3bbd6e05b660c67ed4b49d9f8d5a Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:41:02 +0100 Subject: [PATCH 03/17] Fix missing serialization_params when listing histories in "query mode" --- lib/galaxy/webapps/galaxy/api/histories.py | 4 +++- lib/galaxy/webapps/galaxy/services/histories.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index 410a62410dd..743d31a48cc 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -201,7 +201,9 @@ class FastAPIHistories: offset=offset, search=search, ) - entries, total_matches = self.service.index_query(trans, payload, include_total_count=True) + entries, total_matches = self.service.index_query( + trans, payload, serialization_params, include_total_count=True + ) response.headers["total_matches"] = str(total_matches) return entries diff --git a/lib/galaxy/webapps/galaxy/services/histories.py b/lib/galaxy/webapps/galaxy/services/histories.py index ab3fe0e69dc..f759c97fa35 100644 --- a/lib/galaxy/webapps/galaxy/services/histories.py +++ b/lib/galaxy/webapps/galaxy/services/histories.py @@ -216,6 +216,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): self, trans, payload: HistoryIndexQueryPayload, + serialization_params: SerializationParams, include_total_count: bool = False, ) -> Tuple[List[AnyHistoryView], int]: """Return a list of History accessible by the user @@ -223,7 +224,6 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): :rtype: list :returns: dictionaries containing History details """ - serialization_params = SerializationParams(default_view="detailed") entries, total_matches = self.manager.index_query(trans, payload, include_total_count) return ( [self._serialize_history(trans, entry, serialization_params) for entry in entries], From 2289f6766d0a48590c37b187c5c54889b5522787 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:42:15 +0100 Subject: [PATCH 04/17] Use summary view in the rest of history lists --- client/src/components/Grid/configs/histories.ts | 1 + client/src/components/Grid/configs/historiesShared.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/client/src/components/Grid/configs/histories.ts b/client/src/components/Grid/configs/histories.ts index 8216cda7156..08201f24be0 100644 --- a/client/src/components/Grid/configs/histories.ts +++ b/client/src/components/Grid/configs/histories.ts @@ -32,6 +32,7 @@ type SortKeyLiteral = "create_time" | "name" | "update_time" | undefined; */ async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { const { data, headers } = await historiesFetcher({ + view: "summary", limit, offset, search, diff --git a/client/src/components/Grid/configs/historiesShared.ts b/client/src/components/Grid/configs/historiesShared.ts index 65a65e8a54a..df3fc116438 100644 --- a/client/src/components/Grid/configs/historiesShared.ts +++ b/client/src/components/Grid/configs/historiesShared.ts @@ -22,6 +22,7 @@ type SortKeyLiteral = "create_time" | "name" | "update_time" | undefined; */ async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { const { data, headers } = await historiesFetcher({ + view: "summary", limit, offset, search, From b77ee5c54175d455c427ab6efd5214289b7574ba Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:36:21 +0100 Subject: [PATCH 05/17] Set the default view as summary in index_query For consistency with the existing history index action. --- lib/galaxy/webapps/galaxy/services/histories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/services/histories.py b/lib/galaxy/webapps/galaxy/services/histories.py index f759c97fa35..3d75284d07e 100644 --- a/lib/galaxy/webapps/galaxy/services/histories.py +++ b/lib/galaxy/webapps/galaxy/services/histories.py @@ -226,7 +226,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): """ entries, total_matches = self.manager.index_query(trans, payload, include_total_count) return ( - [self._serialize_history(trans, entry, serialization_params) for entry in entries], + [self._serialize_history(trans, entry, serialization_params, default_view="summary") for entry in entries], total_matches, ) From 216036cbdd0e52141f3ab208a4582b3ef9b2163d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:45:14 +0100 Subject: [PATCH 06/17] Add API tests for history index serialization In both modes --- lib/galaxy_test/api/test_histories.py | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/galaxy_test/api/test_histories.py b/lib/galaxy_test/api/test_histories.py index e263423b2b5..94aabebf5d8 100644 --- a/lib/galaxy_test/api/test_histories.py +++ b/lib/galaxy_test/api/test_histories.py @@ -140,6 +140,56 @@ class TestHistoriesApi(ApiTestCase, BaseHistories): assert len(index_response) == 1 assert index_response[0]["name"] == expected_history_name + def test_index_views(self): + # Make sure there is at least one history + self._create_history(f"TestHistoryForViews_{uuid4()}")["id"] + # By default the view is summary + index_response = self._get("histories").json() + for history in index_response: + assert "state" not in history + + # Change the view to detailed + index_response = self._get("histories?view=detailed").json() + for history in index_response: + assert "state" in history + + # Expect only specific keys + expected_keys = ["name"] + unexpected_keys = ["id", "deleted", "state"] + index_response = self._get(f"histories?keys={','.join(expected_keys)}").json() + for history in index_response: + for key in expected_keys: + assert key in history + for key in unexpected_keys: + assert key not in history + + def test_index_search_mode_views(self): + # Make sure there is at least one history + expected_name_contains = "SearchMode" + self._create_history(f"TestHistory{expected_name_contains}_{uuid4()}")["id"] + # By default the view is summary + data = dict(search=expected_name_contains, show_published=False) + index_response = self._get("histories", data=data).json() + for history in index_response: + assert "state" not in history + + # Change the view to detailed + data = dict(search=expected_name_contains, show_published=False) + index_response = self._get("histories?view=detailed", data=data).json() + for history in index_response: + assert "state" in history + + # Expect only specific keys + expected_keys = ["name"] + unexpected_keys = ["id", "deleted", "state"] + data = dict(search=expected_name_contains, show_published=False, keys=",".join(expected_keys)) + index_response = self._get("histories", data=data).json() + for history in index_response: + for key in expected_keys: + assert key in history + for key in unexpected_keys: + assert key not in history + def test_index_case_insensitive_contains_query(self): # Create the histories with a different user to ensure the test # is not conflicted with the current user's histories. From a7ea52841748437acfb172e11f513eed6f2fcceb Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:10:36 +0100 Subject: [PATCH 07/17] Temporarily avoid checking id not present in API test Until the HistoryMinimal model gets replaced by the CustomHistory again. --- lib/galaxy_test/api/test_histories.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy_test/api/test_histories.py b/lib/galaxy_test/api/test_histories.py index 94aabebf5d8..a3c8e28a9f8 100644 --- a/lib/galaxy_test/api/test_histories.py +++ b/lib/galaxy_test/api/test_histories.py @@ -155,7 +155,7 @@ class TestHistoriesApi(ApiTestCase, BaseHistories): # Expect only specific keys expected_keys = ["name"] - unexpected_keys = ["id", "deleted", "state"] + unexpected_keys = ["deleted", "state"] index_response = self._get(f"histories?keys={','.join(expected_keys)}").json() for history in index_response: for key in expected_keys: @@ -181,7 +181,7 @@ class TestHistoriesApi(ApiTestCase, BaseHistories): # Expect only specific keys expected_keys = ["name"] - unexpected_keys = ["id", "deleted", "state"] + unexpected_keys = ["deleted", "state"] data = dict(search=expected_name_contains, show_published=False, keys=",".join(expected_keys)) index_response = self._get("histories", data=data).json() for history in index_response: From 0def903a895d57f41aa50c5f538b34912dc04d00 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:34:44 +0100 Subject: [PATCH 08/17] fix id unknown --- client/src/stores/userTagsStore.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/client/src/stores/userTagsStore.ts b/client/src/stores/userTagsStore.ts index ce5b449a155..36a20a92b15 100644 --- a/client/src/stores/userTagsStore.ts +++ b/client/src/stores/userTagsStore.ts @@ -20,7 +20,7 @@ class UserTagStoreDatabase extends Dexie { constructor() { super("userTagStoreDatabase"); - this.version(1).stores({ tags: "++id, userHash, lastUsed" }); + this.version(2).stores({ tags: "++id, userHash, lastUsed, tag" }); } } @@ -113,12 +113,17 @@ export const useUserTagsStore = defineStore("userTagsStore", () => { await until(dbLoaded).toBe(true); tag = normalizeTag(tag); - const storedTag = tags.value.find((o) => o.tag === tag); - const id = storedTag?.id; + const dbTag = await db.tags.get({ tag }); - if (id !== undefined) { - // put instead of update, because `removeOldestEntries` may have deleted this tag on init - await db.tags.put({ ...storedTag, lastUsed: Date.now() } as StoredTag, id); + if (dbTag) { + await db.tags.update(dbTag, { lastUsed: Date.now() }); + } else { + const storedTag = tags.value.find((o) => o.tag === tag); + const id = storedTag?.id; + + if (id !== undefined) { + await db.tags.add({ ...storedTag, lastUsed: Date.now() } as StoredTag); + } } } From 61efe13f4711027a5449102c17dba561ea9adbd3 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:45:02 +0100 Subject: [PATCH 09/17] fix sorting and toggling for named tags --- .../src/components/TagsMultiselect/HeadlessMultiselect.vue | 7 +++++-- client/src/components/TagsMultiselect/StatelessTags.vue | 2 +- client/src/stores/userTagsStore.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/components/TagsMultiselect/HeadlessMultiselect.vue b/client/src/components/TagsMultiselect/HeadlessMultiselect.vue index 3a07de33a79..95660d1ced8 100644 --- a/client/src/components/TagsMultiselect/HeadlessMultiselect.vue +++ b/client/src/components/TagsMultiselect/HeadlessMultiselect.vue @@ -15,6 +15,7 @@ import { computed, nextTick, ref, watch } from "vue"; import Vue2Teleport from "vue2-teleport"; import { useUid } from "@/composables/utils/uid"; +import { normalizeTag } from "@/stores/userTagsStore"; library.add(faCheck, faChevronUp, faPlus, faTags, faTimes); @@ -147,6 +148,8 @@ function getOptionWithId(id: number) { * was not part of the provided options */ function onOptionSelected(option: string) { + option = normalizeTag(option); + if (!optionsAsSet.value.has(option)) { emit("addOption", option); return; @@ -318,7 +321,7 @@ whenever(isOpen, async () => { :data-parent-id="props.id" class="headless-multiselect__option" role="option" - :aria-selected="props.selected.includes(option)" + :aria-selected="props.selected.includes(normalizeTag(option))" :class="{ invalid: i === 0 && !searchValueValid, highlighted: highlightedOption === i, @@ -332,7 +335,7 @@ whenever(isOpen, async () => { {{ option }} - +