Skip to content

Commit

Permalink
UI for "relocating" a dataset's object store.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Feb 8, 2024
1 parent ed402d6 commit f2612de
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 16 deletions.
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 @@ -3822,6 +3822,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
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
67 changes: 67 additions & 0 deletions client/src/components/Dataset/DatasetStorage/RelocateDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { ConcreteObjectStoreModel } from "@/api";
import DescribeObjectStore from "@/components/ObjectStore/DescribeObjectStore.vue";
import ObjectStoreSelectButton from "@/components/ObjectStore/ObjectStoreSelectButton.vue";
import ObjectStoreSelectButtonPopover from "@/components/ObjectStore/ObjectStoreSelectButtonPopover.vue";
interface RelocateProps {
fromObjectStore: ConcreteObjectStoreModel;
targetObjectStores: ConcreteObjectStoreModel[];
}
defineProps<RelocateProps>();
const emit = defineEmits<{
(e: "relocate", value: string): void;
}>();
const fromWhat = "This dataset location in a";
const toWhat = "This dataset will be relocated to";
</script>

<template>
<div>
<p>Relocate the dataset's current object store of:</p>
<b-button-group vertical size="lg" class="select-button-group">
<ObjectStoreSelectButton
:id="`swap-target-object-store-button-${fromObjectStore.object_store_id}`"
:key="fromObjectStore.object_store_id"
class="swap-target-object-store-select-button"
variant="info"
:object-store="fromObjectStore" />
</b-button-group>
<p>Select a new object store below to relocate the dataset</p>
<b-button-group vertical size="lg" class="select-button-group">
<ObjectStoreSelectButton
v-for="objectStore in targetObjectStores"
:id="`swap-target-object-store-button-${objectStore.object_store_id}`"
:key="objectStore.object_store_id"
class="swap-target-object-store-select-button"
:data-object-store-id="objectStore.object_store_id"
variant="outline-primary"
:objectStore="objectStore"
@click="emit('relocate', objectStore.object_store_id)" />
</b-button-group>
<ObjectStoreSelectButtonPopover
:target="`swap-target-object-store-button-${fromObjectStore.object_store_id}`"
:title="fromObjectStore.name">
<DescribeObjectStore :what="fromWhat" :storage-info="fromObjectStore"> </DescribeObjectStore>
</ObjectStoreSelectButtonPopover>
<ObjectStoreSelectButtonPopover
v-for="objectStore in targetObjectStores"
:key="objectStore.object_store_id"
:target="`swap-target-object-store-button-${objectStore.object_store_id}`"
:title="objectStore.name">
<DescribeObjectStore :what="toWhat" :storage-info="objectStore"> </DescribeObjectStore>
</ObjectStoreSelectButtonPopover>
</div>
</template>

<style scoped>
.select-button-group {
display: block;
margin: auto;
width: 400px;
}
</style>
107 changes: 107 additions & 0 deletions client/src/components/Dataset/DatasetStorage/RelocateLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
import { ConcreteObjectStoreModel, DatasetStorageDetails } from "@/api";
import { updateObjectStore } from "@/api/objectStores";
import { useObjectStoreStore } from "@/stores/objectStoreStore";
import RelocateModal from "./RelocateModal.vue";
interface RelocateLinkProps {
datasetStorageDetails: DatasetStorageDetails;
datasetId: string;
}
const props = defineProps<RelocateLinkProps>();
const relocateModal = ref<InstanceType<typeof RelocateModal> | null>(null);
function showRelocateDialog() {
// This should work - does it work in proper Vue 3 or something?
// https://vuejs.org/guide/typescript/composition-api.html#typing-component-template-refs
// @ts-ignore
relocateModal.value?.showModal();
}
function hideRelocateDialog() {
// This should work - does it work in proper Vue 3 or something?
// https://vuejs.org/guide/typescript/composition-api.html#typing-component-template-refs
// @ts-ignore
relocateModal.value?.hideModal();
}
const store = useObjectStoreStore();
const { isLoaded, selectableObjectStores } = storeToRefs(store);
const currentObjectStore = computed<ConcreteObjectStoreModel | null>(() => {
const isLoadedVal = isLoaded.value;
const objectStores = selectableObjectStores.value;
const currentObjectStoreId = props.datasetStorageDetails.object_store_id;
if (!isLoadedVal) {
return null;
}
if (!objectStores) {
return null;
}
const filtered: ConcreteObjectStoreModel[] = objectStores.filter(
(objectStore) => objectStore.object_store_id == currentObjectStoreId
);
return filtered && filtered.length > 0 ? (filtered[0] as ConcreteObjectStoreModel) : null;
});
const validTargets = computed<ConcreteObjectStoreModel[]>(() => {
const isLoadedVal = isLoaded.value;
const objectStores = selectableObjectStores.value;
const currentObjectStoreId = props.datasetStorageDetails.object_store_id;
if (!isLoadedVal) {
return [];
}
if (!objectStores) {
return [];
}
if (!currentObjectStore.value) {
return [];
}
const currentDevice = currentObjectStore.value.device;
if (!currentDevice) {
return [];
}
const validTargets: ConcreteObjectStoreModel[] = objectStores.filter(
(objectStore) => objectStore.device == currentDevice && objectStore.object_store_id != currentObjectStoreId
);
return validTargets as ConcreteObjectStoreModel[];
});
const relocatable = computed(() => {
return validTargets.value.length > 0;
});
const emit = defineEmits<{
(e: "relocated"): void;
}>();
async function relocate(objectStoreId: string) {
try {
await updateObjectStore(props.datasetId, objectStoreId);
hideRelocateDialog();
emit("relocated");
} catch (err) {
console.log(err);
}
}
</script>

<template>
<span class="storage-relocate-link">
<RelocateModal
v-if="currentObjectStore"
ref="relocateModal"
:from-object-store="currentObjectStore"
:target-object-stores="validTargets"
@relocate="relocate" />
<b-link v-if="relocatable" href="#" @click="showRelocateDialog">(relocate)</b-link>
</span>
</template>
51 changes: 51 additions & 0 deletions client/src/components/Dataset/DatasetStorage/RelocateModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref } from "vue";
import { ConcreteObjectStoreModel } from "@/api";
import RelocateDialog from "./RelocateDialog.vue";
interface RelocateModalProps {
fromObjectStore: ConcreteObjectStoreModel;
targetObjectStores: ConcreteObjectStoreModel[];
}
defineProps<RelocateModalProps>();
const show = ref(false);
function showModal() {
show.value = true;
}
function hideModal() {
show.value = false;
}
const emit = defineEmits<{
(e: "relocate", value: string): void;
}>();
function relocate(objectStoreId: string) {
emit("relocate", objectStoreId);
}
const title = "Relocate Dataset Storage";
defineExpose({
showModal,
hideModal,
});
</script>

<template>
<b-modal v-model="show" hide-footer>
<template v-slot:modal-title>
<h2>{{ title }}</h2>
</template>
<RelocateDialog
:from-object-store="fromObjectStore"
:target-object-stores="targetObjectStores"
@relocate="relocate" />
</b-modal>
</template>
14 changes: 13 additions & 1 deletion client/src/components/ObjectStore/DescribeObjectStore.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref, watch } from "vue";
import { QuotaSourceUsageProvider } from "@/components/User/DiskUsage/Quota/QuotaUsageProvider.js";
Expand All @@ -21,6 +21,17 @@ const quotaSourceLabel = computed(() => props.storageInfo.quota?.source);
const isPrivate = computed(() => props.storageInfo.private);
const badges = computed(() => props.storageInfo.badges);
const quotaUsageProvider = ref(null);
watch(props, async () => {
if (quotaUsageProvider.value) {
// @ts-ignore
quotaUsageProvider.value.attributes["quotaSourceLabel"] = quotaSourceLabel.value;
// @ts-ignore
quotaUsageProvider.value.doQuery();
}
});
defineExpose({
isPrivate,
});
Expand Down Expand Up @@ -51,6 +62,7 @@ export default {
<ObjectStoreBadges :badges="badges"> </ObjectStoreBadges>
<QuotaSourceUsageProvider
v-if="storageInfo.quota && storageInfo.quota.enabled"
ref="quotaUsageProvider"
v-slot="{ result: quotaUsage, loading: isLoadingUsage }"
:quota-source-label="quotaSourceLabel">
<b-spinner v-if="isLoadingUsage" />
Expand Down
5 changes: 5 additions & 0 deletions lib/galaxy/webapps/galaxy/services/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ class DatasetStorageDetails(Model):
badges: List[BadgeDict] = Field(
description="A list of badges describing object store properties for concrete object store dataset is stored in."
)
relocatable: bool = Field(
description="Indicator of whether the objectstore for this dataset can be switched by this user."
)


class DatasetInheritanceChainEntry(Model):
Expand Down Expand Up @@ -429,6 +432,7 @@ def show_storage(
source=quota_source.label,
enabled=quota_source.use,
)
relocatable = trans.app.security_agent.can_change_object_store_id(trans.user, dataset)
dataset_state = dataset.state
hashes = [h.to_dict() for h in dataset.hashes]
sources = [s.to_dict() for s in dataset.sources]
Expand All @@ -443,6 +447,7 @@ def show_storage(
sources=sources,
quota=quota,
badges=badges,
relocatable=relocatable,
)

def show_inheritance_chain(
Expand Down

0 comments on commit f2612de

Please sign in to comment.