Skip to content

Commit

Permalink
WIP: history filter by objectstore/quotasource
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Feb 13, 2024
1 parent 62ad548 commit f384a65
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 6 deletions.
18 changes: 18 additions & 0 deletions client/src/components/Common/FilterMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import DelayedInput from "@/components/Common/DelayedInput.vue";
import FilterMenuBoolean from "@/components/Common/FilterMenuBoolean.vue";
import FilterMenuInput from "@/components/Common/FilterMenuInput.vue";
import FilterMenuMultiTags from "@/components/Common/FilterMenuMultiTags.vue";
import FilterMenuObjectStore from "@/components/Common/FilterMenuObjectStore.vue";
import FilterMenuQuotaSource from "@/components/Common/FilterMenuQuotaSource.vue";
import FilterMenuRanged from "@/components/Common/FilterMenuRanged.vue";
interface BackendFilterError {
Expand Down Expand Up @@ -202,6 +204,22 @@ watch(
:identifier="identifier"
@change="onOption" />

<FilterMenuObjectStore
v-else-if="validFilters[filter]?.type == 'ObjectStore'"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:identifier="identifier"
@change="onOption" />

<FilterMenuQuotaSource
v-else-if="validFilters[filter]?.type == 'QuotaSource'"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:identifier="identifier"
@change="onOption" />

<FilterMenuInput
v-else
:name="filter"
Expand Down
46 changes: 46 additions & 0 deletions client/src/components/Common/FilterMenuObjectStore.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed, type PropType, ref, watch } from "vue";
import { ValidFilter } from "@/utils/filtering";
import FilterObjectStoreLink from "./FilterObjectStoreLink.vue";
const props = defineProps({
name: { type: String, required: true },
filter: { type: Object as PropType<ValidFilter<any>>, required: true },
filters: { type: Object, required: true },
identifier: { type: String, required: true },
});
const emit = defineEmits<{
(e: "change", name: string, value: string | null): void;
}>();
const propValue = computed(() => props.filters[props.name]);
const localValue = ref(propValue.value);
watch(
() => localValue.value,
(newFilter: string) => {
emit("change", props.name, newFilter);
}
);
watch(
() => propValue.value,
(newFilter: string) => {
localValue.value = newFilter;
}
);
function onChange(value: string | null) {
localValue.value = value;
}
</script>

<template>
<div>
<small>Filter by storage source:</small>
<FilterObjectStoreLink :value="localValue" @change="onChange" />
</div>
</template>
81 changes: 81 additions & 0 deletions client/src/components/Common/FilterMenuQuotaSource.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { computed, onMounted, type PropType, ref, watch } from "vue";
import { QuotaUsage } from "@/components/User/DiskUsage/Quota/model";
import { fetch } from "@/components/User/DiskUsage/Quota/services";
import { ValidFilter } from "@/utils/filtering";
import { errorMessageAsString } from "@/utils/simple-error";
import QuotaUsageBar from "@/components/User/DiskUsage/Quota/QuotaUsageBar.vue";
const props = defineProps({
name: { type: String, required: true },
filter: { type: Object as PropType<ValidFilter<any>>, required: true },
filters: { type: Object, required: true },
identifier: { type: String, required: true },
});
const emit = defineEmits<{
(e: "change", name: string, value: string): void;
}>();
const propValue = computed(() => props.filters[props.name]);
const localValue = ref(propValue.value);
watch(
() => localValue.value,
(newFilter) => {
emit("change", props.name, newFilter);
}
);
watch(
() => propValue.value,
(newFilter: string) => {
localValue.value = newFilter;
}
);
const quotaUsages = ref<QuotaUsage[]>();
const errorMessage = ref<string>();
async function loadQuotaUsages() {
try {
quotaUsages.value = await fetch();
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
}
onMounted(async () => {
await loadQuotaUsages();
});
const dropDownText = computed(() => {
if (localValue.value == null) {
return "(any)";
} else {
return localValue.value.sourceLabel;
}
});
</script>

<template>
<div>
<small>Filter by {{ props.filter.placeholder }}:</small>
<b-input-group :id="`${identifier}-advanced-filter-${props.name}`">
<b-dropdown :text="dropDownText" block class="m-2" size="sm" boundary="window">
<b-dropdown-item href="#" @click="localValue = null"><i>(any)</i></b-dropdown-item>

<b-dropdown-item
v-for="quotaUsage in quotaUsages"
:key="quotaUsage.id"
href="#"
@click="localValue = quotaUsage">
{{ quotaUsage.sourceLabel }}
<QuotaUsageBar :quota-usage="quotaUsage" class="quota-usage-bar" :compact="true" :embedded="true" />
</b-dropdown-item>
</b-dropdown>
</b-input-group>
</div>
</template>
50 changes: 50 additions & 0 deletions client/src/components/Common/FilterObjectStoreLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
import { useObjectStoreStore } from "@/stores/objectStoreStore";
import ObjectStoreSelect from "./ObjectStoreSelect.vue";
import SelectModal from "@/components/Dataset/DatasetStorage/SelectModal.vue";
interface FilterObjectStoreLinkProps {
value: String | null;
}
const props = defineProps<FilterObjectStoreLinkProps>();
const showModal = ref(false);
const store = useObjectStoreStore();
const { selectableObjectStores } = storeToRefs(store);
const hasObjectStores = computed(() => {
return selectableObjectStores.value && selectableObjectStores.value.length > 0;
});
const emit = defineEmits<{
(e: "change", objectStoreId: string | null): void;
}>();
function onSelect(objectStoreId: string | null) {
emit("change", objectStoreId);
showModal.value = false;
}
const selectionText = computed(() => {
if (props.value) {
return props.value;
} else {
return "(any)";
}
});
</script>

<template>
<span class="filter-objectstore-link">
<SelectModal v-model="showModal" title="Select Storage Source">
<ObjectStoreSelect :object-stores="selectableObjectStores" @select="onSelect" />
</SelectModal>
<b-link v-if="hasObjectStores" href="#" @click="showModal = true">{{ selectionText }}</b-link>
</span>
</template>
49 changes: 49 additions & 0 deletions client/src/components/Common/ObjectStoreSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ConcreteObjectStoreModel } from "@/api";
import ObjectStoreSelectButton from "@/components/ObjectStore/ObjectStoreSelectButton.vue";
import ObjectStoreSelectButtonDescribePopover from "@/components/ObjectStore/ObjectStoreSelectButtonDescribePopover.vue";
interface RelocateProps {
objectStores: ConcreteObjectStoreModel[];
}
defineProps<RelocateProps>();
const emit = defineEmits<{
(e: "select", value: string): void;
}>();
const toWhat = "Datasets will be filtered to those stored in";
</script>

<template>
<div>
<p>Select a storage source to filter by</p>
<b-button-group vertical size="lg" class="select-button-group">
<ObjectStoreSelectButton
v-for="objectStore in objectStores"
:key="objectStore.object_store_id"
id-prefix="filter-target"
class="filter-target-object-store-select-button"
variant="outline-primary"
:object-store="objectStore"
@click="emit('select', objectStore.object_store_id)" />
</b-button-group>
<ObjectStoreSelectButtonDescribePopover
v-for="objectStore in objectStores"
:key="objectStore.object_store_id"
id-prefix="filter-target"
:what="toWhat"
:object-store="objectStore" />
<p></p>
</div>
</template>

<style scoped>
.select-button-group {
display: block;
margin: auto;
width: 400px;
}
</style>
15 changes: 15 additions & 0 deletions client/src/components/History/HistoryFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ const validFilters = {
},
genome_build: { placeholder: "database", type: String, handler: contains("genome_build"), menuItem: true },
related: { placeholder: "related", type: Number, handler: equals("related"), menuItem: true },
object_store_id: {
placeholder: "object store",
type: "ObjectStore",
handler: equals("object_store_id"),
menuItem: true,
},
quota_source: {
placeholder: "quota source",
type: "QuotaSource",
handler: equals("quota_source_label", "quota_source_label-eq", (qs) => {
console.log(qs);
return qs ? qs.sourceLabel : null;
}),
menuItem: true,
},
hid: { placeholder: "index", type: Number, handler: equals("hid"), isRangeInput: true, menuItem: true },
hid_ge: { handler: compare("hid", "ge"), menuItem: false },
hid_le: { handler: compare("hid", "le"), menuItem: false },
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/User/DiskUsage/DiskUsageSummary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, watch } from "vue";
import { fetchQuotaUsages, recalculateDiskUsage } from "@/api/users";
import { recalculateDiskUsage } from "@/api/users";
import { useConfig } from "@/composables/config";
import { useTaskMonitor } from "@/composables/taskMonitor";
import { useUserStore } from "@/stores/userStore";
import { errorMessageAsString } from "@/utils/simple-error";
import { bytesToString } from "@/utils/utils";
import { QuotaUsage, UserQuotaUsageData } from "./Quota/model";
import { QuotaUsage } from "./Quota/model";
import { fetch } from "./Quota/services";
import QuotaUsageSummary from "@/components/User/DiskUsage/Quota/QuotaUsageSummary.vue";
Expand Down Expand Up @@ -74,8 +75,7 @@ async function onRefresh() {
async function loadQuotaUsages() {
try {
const { data } = await fetchQuotaUsages({ user_id: "current" });
quotaUsages.value = data.map((u: UserQuotaUsageData) => new QuotaUsage(u));
quotaUsages.value = await fetch();
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
Expand Down
8 changes: 8 additions & 0 deletions client/src/components/User/DiskUsage/Quota/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fetchQuotaUsages } from "@/api/users";

import { QuotaUsage, UserQuotaUsageData } from "./model/index";

export async function fetch() {
const { data } = await fetchQuotaUsages({ user_id: "current" });
return data.map((u: UserQuotaUsageData) => new QuotaUsage(u));
}
4 changes: 4 additions & 0 deletions lib/galaxy/managers/hdas.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ def __init__(self, app: StructuredApp):
"url",
"create_time",
"update_time",
"object_store_id",
"quota_source_label",
],
)
self.add_view(
Expand Down Expand Up @@ -595,6 +597,8 @@ def add_serializers(self):
"api_type": lambda item, key, **context: "file",
"type": lambda item, key, **context: "file",
"created_from_basename": lambda item, key, **context: item.created_from_basename,
"object_store_id": lambda item, key, **context: item.object_store_id,
"quota_source_label": lambda item, key, **context: item.dataset.quota_source_label,
"hashes": lambda item, key, **context: [h.to_dict() for h in item.hashes],
"sources": lambda item, key, **context: [s.to_dict() for s in item.sources],
"drs_id": lambda item, key, **context: f"hda-{self.app.security.encode_id(item.id, kind='drs')}",
Expand Down
24 changes: 24 additions & 0 deletions lib/galaxy/managers/history_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class HistoryContentsManager(base.SortableManager):
"collection_id",
"name",
"state",
"object_store_id",
"size",
"deleted",
"purged",
Expand Down Expand Up @@ -354,6 +355,8 @@ def _contents_common_query_for_contained(self, history_id, user_id):
history_content_type=literal("dataset"),
size=model.Dataset.file_size,
state=model.Dataset.state,
object_store_id=model.Dataset.object_store_id,
quota_source_label=model.Dataset.object_store_id,
# do not have inner collections
collection_id=literal(None),
)
Expand All @@ -380,6 +383,8 @@ def _contents_common_query_for_subcontainer(self, history_id, user_id):
dataset_id=literal(None),
size=literal(None),
state=model.DatasetCollection.populated_state,
object_store_id=literal(None),
quota_source_label=literal(None),
# TODO: should be purgable? fix
purged=literal(False),
extension=literal(None),
Expand Down Expand Up @@ -569,6 +574,23 @@ def get_filter(attr, op, val):
return sql.column("state").in_(states)
raise_filter_err(attr, op, val, "bad op in filter")

if attr == "object_store_id":
if op == "eq":
return sql.column("object_store_id") == val

if op == "in":
object_store_ids = [s for s in val.split(",") if s]
return sql.column("object_store_id").in_(object_store_ids)

raise_filter_err(attr, op, val, "bad op in filter")

if attr == "quota_source_label":
if op == "eq":
ids = self.app.object_store.get_quota_source_map().ids_per_quota_source()
# todo handle null and such better here...
object_store_ids = ids[val]
return sql.column("object_store_id").in_(object_store_ids)

if (column_filter := get_filter(attr, op, val)) is not None:
return self.parsed_filter(filter_type="orm", filter=column_filter)
return super()._parse_orm_filter(attr, op, val)
Expand Down Expand Up @@ -625,6 +647,8 @@ def _add_parsers(self):
# 'hid-in' : { 'op': ( 'in' ), 'val': self.parse_int_list },
"name": {"op": ("eq", "contains", "like")},
"state": {"op": ("eq", "in")},
"object_store_id": {"op": ("eq", "in")},
"quota_source_label": {"op": ("eq")},
"visible": {"op": ("eq"), "val": parse_bool},
"create_time": {"op": ("le", "ge", "lt", "gt"), "val": self.parse_date},
"update_time": {"op": ("le", "ge", "lt", "gt"), "val": self.parse_date},
Expand Down
Loading

0 comments on commit f384a65

Please sign in to comment.