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 15, 2024
1 parent fb7e244 commit fcdcd8d
Show file tree
Hide file tree
Showing 16 changed files with 425 additions and 14 deletions.
15 changes: 15 additions & 0 deletions client/src/components/Common/FilterMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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";
library.add(faAngleDoubleUp, faQuestion, faRedo, faSearch);
Expand Down Expand Up @@ -206,6 +208,19 @@ watch(
:filters="filters"
:identifier="identifier"
@change="onOption" />
<FilterMenuObjectStore
v-else-if="validFilters[filter]?.type == 'ObjectStore'"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
@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
60 changes: 60 additions & 0 deletions client/src/components/Common/FilterMenuObjectStore.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { computed, ref, watch } from "vue";
import { useObjectStoreStore } from "@/stores/objectStoreStore";
import { ValidFilter } from "@/utils/filtering";
import FilterObjectStoreLink from "./FilterObjectStoreLink.vue";
type FilterType = string | boolean | undefined;
interface Props {
name: string;
filter: ValidFilter<any>;
filters: {
[k: string]: FilterType;
};
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "change", name: string, value: FilterType): void;
}>();
const propValue = computed<FilterType>(() => props.filters[props.name]);
const localValue = ref<FilterType>(propValue.value);
watch(
() => localValue.value,
(newFilter: FilterType) => {
emit("change", props.name, newFilter);
}
);
watch(
() => propValue.value,
(newFilter: FilterType) => {
localValue.value = newFilter;
}
);
const store = useObjectStoreStore();
const { selectableObjectStores } = storeToRefs(store);
const hasObjectStores = computed(() => {
return selectableObjectStores.value && selectableObjectStores.value.length > 0;
});
function onChange(value: string | null) {
localValue.value = (value || undefined) as FilterType;
}
</script>

<template>
<div v-if="hasObjectStores">
<small>Filter by storage source:</small>
<FilterObjectStoreLink :object-stores="selectableObjectStores" :value="localValue" @change="onChange" />
</div>
</template>
101 changes: 101 additions & 0 deletions client/src/components/Common/FilterMenuQuotaSource.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, onMounted, ref, type UnwrapRef, watch } from "vue";
import { type 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";
type QuotaUsageUnwrapped = UnwrapRef<QuotaUsage>;
type FilterType = QuotaUsageUnwrapped | string | boolean | undefined;
interface Props {
name: string;
filter: ValidFilter<any>;
filters: {
[k: string]: FilterType;
};
identifier: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "change", name: string, value: FilterType): void;
}>();
const propValue = computed<FilterType>(() => props.filters[props.name]);
const localValue = ref<FilterType>(propValue.value);
watch(
() => localValue.value,
() => {
emit("change", props.name, localValue.value);
}
);
watch(
() => propValue.value,
() => {
localValue.value = propValue.value;
}
);
const quotaUsages = ref<QuotaUsage[]>([] as QuotaUsage[]);
const errorMessage = ref<string>();
async function loadQuotaUsages() {
try {
quotaUsages.value = await fetch();
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
}
const hasMultipleQuotaSources = computed<boolean>(() => {
return !!(quotaUsages.value && quotaUsages.value.length > 1);
});
onMounted(async () => {
await loadQuotaUsages();
});
function isQuotaUsage(value: FilterType): value is QuotaUsageUnwrapped {
return !!(value && value instanceof Object && "rawSourceLabel" in value);
}
const dropDownText = computed<string>(() => {
if (isQuotaUsage(localValue.value)) {
return localValue.value.sourceLabel;
} else {
return "(any)";
}
});
function setValue(val: QuotaUsage | undefined) {
localValue.value = val;
}
</script>

<template>
<div v-if="hasMultipleQuotaSources">
<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="setValue(undefined)"><i>(any)</i></b-dropdown-item>

<b-dropdown-item
v-for="quotaUsage in quotaUsages"
:key="quotaUsage.id"
href="#"
@click="setValue(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>
51 changes: 51 additions & 0 deletions client/src/components/Common/FilterObjectStoreLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, ref } from "vue";
import { ConcreteObjectStoreModel } from "@/api";
import ObjectStoreSelect from "./ObjectStoreSelect.vue";
import SelectModal from "@/components/Dataset/DatasetStorage/SelectModal.vue";
library.add(faTimes);
interface FilterObjectStoreLinkProps {
value: String | null;
objectStores: ConcreteObjectStoreModel[];
}
const props = defineProps<FilterObjectStoreLinkProps>();
const showModal = ref(false);
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="objectStores" @select="onSelect" />
</SelectModal>
<b-link href="#" @click="showModal = true">{{ selectionText }}</b-link>
<span v-if="value" v-b-tooltip.hover title="Remove Filter">
<FontAwesomeIcon icon="times" @click="onSelect(null)" />
</span>
</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>
36 changes: 35 additions & 1 deletion client/src/components/History/HistoryFilters.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { STATES } from "components/History/Content/model/states";
import StatesInfo from "components/History/Content/model/StatesInfo";
import Filtering, { compare, contains, equals, expandNameTag, toBool, toDate } from "utils/filtering";
import Filtering, {
compare,
contains,
equals,
expandNameTag,
quotaSourceFilter,
toBool,
toDate,
} from "utils/filtering";

const excludeStates = ["empty", "failed", "upload", "placeholder", "failed_populated_state", "new_populated_state"];
const states = Object.keys(STATES).filter((state) => !excludeStates.includes(state));
Expand All @@ -19,6 +27,32 @@ 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: quotaSourceFilter("quota_source_label", (qs) => {
if (!qs) {
return null;
}
if (typeof qs === "string" || qs instanceof String) {
return qs;
}
const label = qs.rawSourceLabel;
if (label == null) {
// API uses this as the label for the label-less default quota
return "__null__";
} else {
return label;
}
}),
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ export class QuotaUsage {
this._data = data;
}

get rawSourceLabel(): string | null {
return this._data.quota_source_label ?? null;
}

/**
* The name of the ObjectStore associated with the quota.
*/
get sourceLabel(): string {
return this._data.quota_source_label ?? DEFAULT_QUOTA_SOURCE_LABEL;
return this.rawSourceLabel ?? DEFAULT_QUOTA_SOURCE_LABEL;
}

/**
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));
}
Loading

0 comments on commit fcdcd8d

Please sign in to comment.