Skip to content

Commit

Permalink
Merge pull request #17437 from jmchilton/change_os_gui
Browse files Browse the repository at this point in the history
UI for "relocating" a dataset to a new object store (when safe)
  • Loading branch information
mvdbeek authored Feb 16, 2024
2 parents 35c0fd2 + 5f67572 commit 8f956cb
Show file tree
Hide file tree
Showing 17 changed files with 413 additions and 81 deletions.
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
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
63 changes: 63 additions & 0 deletions client/src/components/Dataset/DatasetStorage/RelocateDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { ConcreteObjectStoreModel } from "@/api";
import ObjectStoreSelectButton from "@/components/ObjectStore/ObjectStoreSelectButton.vue";
import ObjectStoreSelectButtonDescribePopover from "@/components/ObjectStore/ObjectStoreSelectButtonDescribePopover.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
:key="fromObjectStore.object_store_id"
id-prefix="swap-target"
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"
:key="objectStore.object_store_id"
id-prefix="swap-target"
class="swap-target-object-store-select-button"
variant="outline-primary"
:object-store="objectStore"
@click="emit('relocate', objectStore.object_store_id)" />
</b-button-group>
<ObjectStoreSelectButtonDescribePopover
id-prefix="swap-target"
:what="fromWhat"
:object-store="fromObjectStore" />
<ObjectStoreSelectButtonDescribePopover
v-for="objectStore in targetObjectStores"
:key="objectStore.object_store_id"
id-prefix="swap-target"
:what="toWhat"
:object-store="objectStore" />
</div>
</template>

<style scoped>
.select-button-group {
display: block;
margin: auto;
width: 400px;
}
</style>
95 changes: 95 additions & 0 deletions client/src/components/Dataset/DatasetStorage/RelocateLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<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 RelocateDialog from "./RelocateDialog.vue";
import SelectModal from "./SelectModal.vue";
interface RelocateLinkProps {
datasetStorageDetails: DatasetStorageDetails;
datasetId: string;
}
const props = defineProps<RelocateLinkProps>();
const showModal = ref(false);
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);
emit("relocated");
} catch (err) {
console.log(err);
} finally {
showModal.value = false;
}
}
</script>

<template>
<span class="storage-relocate-link">
<SelectModal v-if="currentObjectStore" v-model="showModal" title="Relocate Dataset Storage">
<RelocateDialog
:from-object-store="currentObjectStore"
:target-object-stores="validTargets"
@relocate="relocate" />
</SelectModal>
<b-link v-if="relocatable" href="#" @click="showModal = true">(relocate)</b-link>
</span>
</template>
33 changes: 33 additions & 0 deletions client/src/components/Dataset/DatasetStorage/SelectModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref, watch } from "vue";
interface SelectModalProps {
value: boolean;
title: string;
}
const props = defineProps<SelectModalProps>();
const show = ref(props.value);
watch(props, () => {
show.value = props.value;
});
const emit = defineEmits<{
(e: "input", value: boolean): void;
}>();
watch(show, () => {
emit("input", show.value);
});
</script>

<template>
<b-modal v-model="show" hide-footer>
<template v-slot:modal-title>
<h2>{{ title }}</h2>
</template>
<slot />
</b-modal>
</template>
12 changes: 11 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,15 @@ 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.update({ quotaSourceLabel: quotaSourceLabel.value });
}
});
defineExpose({
isPrivate,
});
Expand Down Expand Up @@ -51,6 +60,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
32 changes: 32 additions & 0 deletions client/src/components/ObjectStore/ObjectStoreSelectButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import { BButton } from "bootstrap-vue";
import { type ConcreteObjectStoreModel } from "@/api";
import ObjectStoreBadges from "@/components/ObjectStore/ObjectStoreBadges.vue";
import ProvidedQuotaSourceUsageBar from "@/components/User/DiskUsage/Quota/ProvidedQuotaSourceUsageBar.vue";
interface ObjectStoreSelectButtonProps {
objectStore: ConcreteObjectStoreModel;
variant: string;
idPrefix: string;
}
defineProps<ObjectStoreSelectButtonProps>();
const emit = defineEmits<{
(e: "click", value: string): void;
}>();
</script>

<template>
<BButton
:id="`${idPrefix}-object-store-button-${objectStore.object_store_id}`"
:variant="variant"
:data-object-store-id="objectStore.object_store_id"
@click="emit('click', objectStore.object_store_id)"
>{{ objectStore.name }}
<ObjectStoreBadges :badges="objectStore.badges" size="lg" :more-on-hover="false" />
<ProvidedQuotaSourceUsageBar :object-store="objectStore" :compact="true"> </ProvidedQuotaSourceUsageBar>
</BButton>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { computed } from "vue";
import { type ConcreteObjectStoreModel } from "@/api";
import DescribeObjectStore from "./DescribeObjectStore.vue";
import ObjectStoreSelectButtonPopover from "./ObjectStoreSelectButtonPopover.vue";
interface ObjectStoreSelectButtonPopoverDescribeProps {
idPrefix: string;
what: string;
objectStore: ConcreteObjectStoreModel;
}
const props = defineProps<ObjectStoreSelectButtonPopoverDescribeProps>();
const target = computed(() => {
return `${props.idPrefix}-object-store-button-${props.objectStore.object_store_id}`;
});
const title = computed(() => {
return props.objectStore.name;
});
</script>

<template>
<ObjectStoreSelectButtonPopover :target="target" :title="title">
<DescribeObjectStore :what="what" :storage-info="objectStore"> </DescribeObjectStore>
</ObjectStoreSelectButtonPopover>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { BPopover } from "bootstrap-vue";
interface ObjectStoreSelectButtonPopoverProps {
target: string;
title: string;
}
defineProps<ObjectStoreSelectButtonPopoverProps>();
const boundary = "window"; // don't warp the popover to squeeze it into this modal
</script>

<template>
<BPopover :target="target" triggers="hover" placement="rightbottom" :boundary="boundary">
<template v-slot:title>{{ title }}</template>
<slot />
</BPopover>
</template>
Loading

0 comments on commit 8f956cb

Please sign in to comment.