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

Allow filtering history datasets by object store ID and quota source. #17460

Merged
merged 8 commits into from
Feb 20, 2024
2 changes: 2 additions & 0 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export type DCObject = components["schemas"]["DCObject"];

export type DatasetCollectionAttributes = components["schemas"]["DatasetCollectionAttributesResult"];

export type ConcreteObjectStoreModel = components["schemas"]["ConcreteObjectStoreModel"];

/**
* A SubCollection is a DatasetCollectionElement of type `dataset_collection`
* with additional information to simplify its handling.
Expand Down
7 changes: 7 additions & 0 deletions client/src/api/objectStores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ export async function getObjectStoreDetails(id: string) {
const { data } = await getObjectStore({ object_store_id: id });
return data;
}

const updateObjectStoreFetcher = fetcher.path("/api/datasets/{dataset_id}/object_store_id").method("put").create();

export async function updateObjectStore(datasetId: string, objectStoreId: string) {
const { data } = await updateObjectStoreFetcher({ dataset_id: datasetId, object_store_id: objectStoreId });
return data;
}
5 changes: 5 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3823,6 +3823,11 @@ export interface components {
percent_used: number | null;
/** @description Information about quota sources around dataset storage. */
quota: components["schemas"]["ConcreteObjectStoreQuotaSourceDetails"];
/**
* Relocatable
* @description Indicator of whether the objectstore for this dataset can be switched by this user.
*/
relocatable: boolean;
/**
* Shareable
* @description Is this dataset shareable.
Expand Down
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: 21 additions & 15 deletions client/src/components/Dataset/DatasetStorage/DatasetStorage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DatasetStorageDetails } from "@/api";
import { fetchDatasetStorage } from "@/api/datasets";
import { errorMessageAsString } from "@/utils/simple-error";

import RelocateLink from "./RelocateLink.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
import DescribeObjectStore from "@/components/ObjectStore/DescribeObjectStore.vue";

Expand Down Expand Up @@ -42,25 +43,30 @@ const sourceUri = computed(() => {
return rootSources[0]?.source_uri;
});

watch(
props,
async () => {
const datasetId = props.datasetId;
const datasetType = props.datasetType;
try {
const response = await fetchDatasetStorage({ dataset_id: datasetId, hda_ldda: datasetType });
storageInfo.value = response.data;
} catch (error) {
errorMessage.value = errorMessageAsString(error);
}
},
{ immediate: true }
);
async function fetch() {
const datasetId = props.datasetId;
const datasetType = props.datasetType;
try {
const response = await fetchDatasetStorage({ dataset_id: datasetId, hda_ldda: datasetType });
storageInfo.value = response.data;
} catch (error) {
errorMessage.value = errorMessageAsString(error);
}
}

watch(props, fetch, { immediate: true });
</script>

<template>
<div>
<h2 v-if="includeTitle" class="h-md">Dataset Storage</h2>
<h2 v-if="includeTitle" class="h-md">
Dataset Storage
<RelocateLink
v-if="storageInfo"
:dataset-id="datasetId"
:dataset-storage-details="storageInfo"
@relocated="fetch" />
</h2>
<div v-if="errorMessage" class="error">{{ errorMessage }}</div>
<LoadingSpan v-else-if="storageInfo == null"> </LoadingSpan>
<div v-else-if="discarded">
Expand Down
Loading
Loading