diff --git a/src/components/object-storage/BucketListItem.vue b/src/components/object-storage/BucketListItem.vue index 7833d851fe1df08e42bd70353df79bee010eb4e5..0650a4c35ab93069487ac4862744d295f9afe01d 100644 --- a/src/components/object-storage/BucketListItem.vue +++ b/src/components/object-storage/BucketListItem.vue @@ -1,9 +1,5 @@ <script setup lang="ts"> -import type { - BucketOut, - BucketPermissionIn, - BucketPermissionOut, -} from "@/client/s3proxy"; +import type { BucketOut, BucketPermissionOut } from "@/client/s3proxy"; import { Constraint } from "@/client/s3proxy"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import PermissionModal from "@/components/object-storage/modals/PermissionModal.vue"; @@ -15,6 +11,7 @@ import { Tooltip } from "bootstrap"; import { useBucketStore } from "@/stores/buckets"; import { useRouter } from "vue-router"; import { useAuthStore } from "@/stores/users"; +import type { FolderTree } from "@/types/PseudoFolder"; const props = defineProps<{ active: boolean; @@ -31,13 +28,19 @@ const router = useRouter(); const permission = computed<BucketPermissionOut | undefined>( () => permissionRepository.ownPermissions[props.bucket.name], ); +const subFolder = computed<FolderTree>(() => { + const subFolders: Record<string, FolderTree> = {}; + if (permission.value?.file_prefix != null) { + subFolders[permission.value.file_prefix] = { subFolders: {}, files: [] }; + } + return { subFolders: subFolders, files: [] }; +}); const emit = defineEmits<{ (e: "delete-bucket", bucketName: string): void; }>(); -function permissionDeleted(perm: BucketPermissionIn) { - permissionRepository.deleteOwnPermission(perm.bucket_name); +function permissionDeleted() { router.push({ name: "buckets" }); } @@ -55,7 +58,7 @@ onMounted(() => { v-if="permission != undefined && props.active" :modalID="'view-permission-modal' + randomIDSuffix" :bucket-name="props.bucket.name" - :sub-folders="{ subFolders: {}, files: [] }" + :sub-folders="subFolder" :edit-user-permission="permission" :readonly="true" :editable="false" diff --git a/src/components/object-storage/modals/CopyObjectModal.vue b/src/components/object-storage/modals/CopyObjectModal.vue index 32313142e8a8b9acfaadebd1353f0b67fa12c124..0687aaa6a9ceee3c848e94eac1a588108848a6fb 100644 --- a/src/components/object-storage/modals/CopyObjectModal.vue +++ b/src/components/object-storage/modals/CopyObjectModal.vue @@ -1,17 +1,17 @@ <script setup lang="ts"> -import type { S3Client } from "@aws-sdk/client-s3"; -import { CopyObjectCommand } from "@aws-sdk/client-s3"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { Modal, Toast } from "bootstrap"; import { onMounted, reactive, watch } from "vue"; -import type { S3ObjectMetaInformation } from "@/client/s3proxy"; -import dayjs from "dayjs"; +import type { _Object as S3Object } from "@aws-sdk/client-s3"; import { useBucketStore } from "@/stores/buckets"; +import { useS3ObjectStore } from "@/stores/s3objects"; + +const objectRepository = useS3ObjectStore(); const props = defineProps<{ modalID: string; - sourceObject: S3ObjectMetaInformation; - s3Client: S3Client; + srcObject: S3Object; + srcBucket: string; }>(); const formState = reactive<{ @@ -25,39 +25,32 @@ const formState = reactive<{ }); const bucketRepository = useBucketStore(); -const emit = defineEmits<{ - (e: "object-copied", object: S3ObjectMetaInformation): void; -}>(); - const randomIDSuffix = Math.random().toString(16).substring(2, 8); let copyModal: Modal | null = null; let successToast: Toast | null = null; let errorToast: Toast | null = null; -function getFileName(key: string): string { +function getFileName(key?: string): string { + if (key == undefined) { + return ""; + } const splittedKey = key.split("/"); return splittedKey[splittedKey.length - 1]; } function copyObject() { - const command = new CopyObjectCommand({ - Bucket: formState.destBucket, - CopySource: encodeURI( - `/${props.sourceObject.bucket}/${props.sourceObject.key}`, - ), - Key: formState.destKey, - }); + if (props.srcObject.Key == undefined) { + return; + } formState.uploading = true; - props.s3Client - .send(command) + objectRepository + .copyObject( + props.srcBucket, + props.srcObject, + formState.destBucket, + formState.destKey, + ) .then(() => { - emit("object-copied", { - key: formState.destKey, - bucket: formState.destBucket, - size: props.sourceObject.size, - last_modified: dayjs().toISOString(), - content_type: props.sourceObject.content_type, - }); copyModal?.hide(); successToast?.show(); formState.destBucket = ""; @@ -76,9 +69,9 @@ function modalClosed() { } watch( - () => props.sourceObject.key, + () => props.srcObject.Key, (newKey) => { - formState.destKey = newKey; + formState.destKey = newKey ?? ""; }, ); @@ -140,7 +133,7 @@ onMounted(() => { v-on="{ 'hidden.bs.modal': modalClosed }" > <template v-slot:header> - <h4>Copy file {{ getFileName(props.sourceObject.key) }}</h4> + <h4>Copy file {{ getFileName(props.srcObject.Key) }}</h4> </template> <template v-slot:body> <div class="container-fluid"> diff --git a/src/components/object-storage/modals/CreateFolderModal.vue b/src/components/object-storage/modals/CreateFolderModal.vue index 627b3a8428c672e99c2cffeb8c9e25c7bfd51203..cd26d2c2a94ff03d6abe9a68e0f2fb5e623a081a 100644 --- a/src/components/object-storage/modals/CreateFolderModal.vue +++ b/src/components/object-storage/modals/CreateFolderModal.vue @@ -1,17 +1,16 @@ <script setup lang="ts"> -import type { S3Client } from "@aws-sdk/client-s3"; -import { PutObjectCommand } from "@aws-sdk/client-s3"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { computed, onMounted, reactive } from "vue"; -import type { S3ObjectMetaInformation } from "@/client/s3proxy"; -import dayjs from "dayjs"; + import { Modal, Toast } from "bootstrap"; +import { useS3ObjectStore } from "@/stores/s3objects"; + +const objectRepository = useS3ObjectStore(); const props = defineProps<{ modalID: string; bucketName: string; keyPrefix: string; - s3Client: S3Client; }>(); const randomIDSuffix = Math.random().toString(16).substring(2, 8); @@ -21,10 +20,6 @@ let errorToast: Toast | null = null; const currentFolders = computed<string[]>(() => props.keyPrefix.split("/")); -const emit = defineEmits<{ - (e: "folder-created", object: S3ObjectMetaInformation): void; -}>(); - const formState = reactive<{ folderName: string; uploading: boolean; @@ -47,25 +42,12 @@ function uploadFolder() { return; } const realKey = reversedKey.slice(firstLetterIndex).reverse().join("") + "/"; - const command = new PutObjectCommand({ - Bucket: props.bucketName, - Body: "", - ContentType: "text/plain", - Key: realKey, - }); formState.uploading = true; - props.s3Client - .send(command) + objectRepository + .createFolder(props.bucketName, realKey) .then(() => { uploadModal?.hide(); successToast?.show(); - emit("folder-created", { - key: realKey, - bucket: props.bucketName, - size: 0, - last_modified: dayjs().toISOString(), - content_type: "text/plain", - }); formState.folderName = ""; }) .catch((e) => { diff --git a/src/components/object-storage/modals/ObjectDetailModal.vue b/src/components/object-storage/modals/ObjectDetailModal.vue index 023b1ca1d910f949ee621711136373a879236b78..9de29087f9c9d5cabc4e6f4d56aaba864808f23a 100644 --- a/src/components/object-storage/modals/ObjectDetailModal.vue +++ b/src/components/object-storage/modals/ObjectDetailModal.vue @@ -1,12 +1,13 @@ <script setup lang="ts"> import BootstrapModal from "@/components/modals/BootstrapModal.vue"; -import type { S3ObjectMetaInformation } from "@/client/s3proxy"; +import type { _Object as S3Object } from "@aws-sdk/client-s3"; import dayjs from "dayjs"; import { filesize } from "filesize"; const props = defineProps<{ modalID: string; - s3Object: S3ObjectMetaInformation; + s3Object: S3Object; + bucket: string; }>(); </script> @@ -25,17 +26,20 @@ const props = defineProps<{ <tbody> <tr> <th scope="row" class="col-4">Bucket</th> - <td class="col-8">{{ props.s3Object.bucket }}</td> + <td class="col-8">{{ props.bucket }}</td> </tr> <tr> <th scope="row">Name</th> - <td>{{ props.s3Object.key }}</td> + <td>{{ props.s3Object.Key }}</td> </tr> <tr> <th scope="row">Size</th> <td> {{ - filesize(props.s3Object.size, { base: 2, standard: "jedec" }) + filesize(props.s3Object.Size ?? 0, { + base: 2, + standard: "jedec", + }) }} </td> </tr> @@ -43,7 +47,7 @@ const props = defineProps<{ <th scope="row">Timestamp</th> <td> {{ - dayjs(props.s3Object.last_modified).format( + dayjs(props.s3Object.LastModified).format( "YYYY-MM-DD HH:mm:ss", ) }} @@ -51,7 +55,7 @@ const props = defineProps<{ </tr> <tr> <th scope="row">Content Type</th> - <td>{{ props.s3Object.content_type }}</td> + <td>binary/octet-stream</td> </tr> </tbody> </table> diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue index 1f9596cf1a313664b15198f2aedcaf3fa7a92b0b..485f1ace427930c8cd8bb0e36af5a00ec1ec9b8f 100644 --- a/src/components/object-storage/modals/PermissionModal.vue +++ b/src/components/object-storage/modals/PermissionModal.vue @@ -31,6 +31,8 @@ const props = defineProps<{ }>(); const bucketRepository = useBucketStore(); + +const emit = defineEmits<{ (e: "permission-deleted"): void }>(); // Variables // ----------------------------------------------------------------------------- const randomIDSuffix = Math.random().toString(16).substring(2, 8); @@ -158,7 +160,7 @@ function findSubFolders( const subFolderString = (parentFolders.length > 0 ? parentFolders.join("/") + "/" : "") + subFolder + - "/"; + (subFolder.endsWith("/") ? "" : "/"); arr.push( subFolderString, ...findSubFolders( @@ -216,6 +218,7 @@ function confirmedDeletePermission(bucketName: string, uid: string) { .deleteBucketPermission(bucketName, uid) .then(() => { permissionDeleted.value = true; + emit("permission-deleted"); permissionModal?.hide(); successToast?.show(); }) diff --git a/src/components/object-storage/modals/UploadObjectModal.vue b/src/components/object-storage/modals/UploadObjectModal.vue index e59d9e99346ba442ea9e8383d659f58cd6ca793e..adafe9a20af1735e997005d1737f9b3dc2edaa1b 100644 --- a/src/components/object-storage/modals/UploadObjectModal.vue +++ b/src/components/object-storage/modals/UploadObjectModal.vue @@ -1,20 +1,17 @@ <script setup lang="ts"> -import type { S3Client } from "@aws-sdk/client-s3"; -import { Upload } from "@aws-sdk/lib-storage"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { computed, onMounted, reactive, ref, watch } from "vue"; -import type { S3ObjectMetaInformation } from "@/client/s3proxy"; -import dayjs from "dayjs"; import { Modal, Toast } from "bootstrap"; import { partial } from "filesize"; +import { useS3ObjectStore } from "@/stores/s3objects"; const fsize = partial({ base: 2, standard: "jedec" }); +const objectRepository = useS3ObjectStore(); const props = defineProps<{ modalID: string; bucketName: string; keyPrefix: string; - s3Client: S3Client; editObjectFileName?: string; }>(); @@ -26,10 +23,6 @@ let errorToast: Toast | null = null; const currentFolders = computed<string[]>(() => props.keyPrefix.split("/")); -const emit = defineEmits<{ - (e: "object-created", object: S3ObjectMetaInformation): void; -}>(); - watch( () => props.editObjectFileName, (nextFileName) => { @@ -59,52 +52,35 @@ const editObject = computed<boolean>( () => props.editObjectFileName !== undefined, ); -async function uploadObject() { +function uploadObject() { const key = props.keyPrefix.length > 0 ? props.keyPrefix + "/" + formState.key : formState.key; - try { - formState.uploadDone = 0; - formState.uploading = true; - const parallelUploads3 = new Upload({ - client: props.s3Client, - params: { - Bucket: props.bucketName, - Body: formState.file, - ContentType: formState.file.type, - Key: key, - }, - queueSize: 4, // optional concurrency configuration - leavePartsOnError: false, // optional manually handle dropped parts - }); - - parallelUploads3.on("httpUploadProgress", (progress) => { + formState.uploadDone = 0; + formState.uploading = true; + objectRepository + .uploadObjectFile(props.bucketName, key, formState.file, (progress) => { if (progress.loaded != null && progress.total != null) { formState.uploadDone = progress.loaded; formState.uploadTotal = progress.total; } + }) + .then(() => { + uploadModal?.hide(); + successToast?.show(); + formState.key = ""; + if (objectFileInput.value != undefined) { + objectFileInput.value.value = ""; + } + }) + .catch((e) => { + console.error(e); + errorToast?.show(); + }) + .finally(() => { + formState.uploading = false; }); - await parallelUploads3.done(); - uploadModal?.hide(); - successToast?.show(); - emit("object-created", { - key: key, - bucket: props.bucketName, - size: formState.file?.size ?? 0, - last_modified: dayjs().toISOString(), - content_type: formState.file?.type ?? "binary/octet-stream", - }); - formState.key = ""; - if (objectFileInput.value != undefined) { - objectFileInput.value.value = ""; - } - } catch (e) { - console.error(e); - errorToast?.show(); - } finally { - formState.uploading = false; - } } function fileChange() { diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue index ae1a09d8a07e4f99cb4d865598e5c3876254d727..fea414f4cff689c1cd46af8bc682c17dd62b1d78 100644 --- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue +++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue @@ -6,6 +6,11 @@ import Ajv from "ajv"; import type { ValidateFunction } from "ajv"; import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue"; import { Toast } from "bootstrap"; +import { useBucketStore } from "@/stores/buckets"; +import { useS3KeyStore } from "@/stores/s3keys"; + +const bucketRepository = useBucketStore(); +const s3KeyRepository = useS3KeyStore(); // Props // ============================================================================= @@ -165,6 +170,9 @@ function startWorkflow() { // ============================================================================= onMounted(() => { if (props.schema) updateSchema(props.schema); + bucketRepository.fetchBuckets(); + bucketRepository.fetchOwnPermissions(); + s3KeyRepository.fetchS3Keys(); errorToast = new Toast("#workflowExecutionErrorToast"); }); </script> @@ -208,6 +216,18 @@ onMounted(() => { @submit.prevent="startWorkflow" novalidate > + <template v-for="(group, groupName) in parameterGroups" :key="groupName"> + <parameter-group-form + :modelValue="formState.formInput[groupName]" + @update:model-value=" + (newValue) => (formState.formInput[groupName] = newValue) + " + v-if="formState.formInput[groupName]" + :parameter-group-name="groupName" + :parameter-group="group" + :showHidden="formState.showHidden" + /> + </template> <div class="card mb-3"> <h2 class="card-header" id="pipelineGeneralOptions"> <font-awesome-icon icon="fa-solid fa-gear" class="me-2" /> @@ -256,18 +276,6 @@ onMounted(() => { </label> </div> </div> - <template v-for="(group, groupName) in parameterGroups" :key="groupName"> - <parameter-group-form - :modelValue="formState.formInput[groupName]" - @update:model-value=" - (newValue) => (formState.formInput[groupName] = newValue) - " - v-if="formState.formInput[groupName]" - :parameter-group-name="groupName" - :parameter-group="group" - :showHidden="formState.showHidden" - /> - </template> </form> <!-- Loading card --> <div v-else class="col-9"> @@ -317,12 +325,6 @@ onMounted(() => { <nav class="h-100"> <nav v-if="props.schema" class="nav"> <ul class="ps-0"> - <li class="nav-link"> - <a href="#pipelineGeneralOptions"> - <font-awesome-icon icon="fa-solid fa-gear" class="me-2" /> - General Pipeline Options - </a> - </li> <li class="nav-link" v-for="group in navParameterGroups" @@ -337,6 +339,12 @@ onMounted(() => { {{ group.title }}</a > </li> + <li class="nav-link"> + <a href="#pipelineGeneralOptions"> + <font-awesome-icon icon="fa-solid fa-gear" class="me-2" /> + General Pipeline Options + </a> + </li> </ul> <div class="mx-auto mb-3"> <input diff --git a/src/components/parameter-schema/form-mode/ParameterStringInput.vue b/src/components/parameter-schema/form-mode/ParameterStringInput.vue index 7eca836677b4f78b8d6e2069333c6e9a706b5016..cbfa064affa80a82cc437d637b36fa95eaf2ccf8 100644 --- a/src/components/parameter-schema/form-mode/ParameterStringInput.vue +++ b/src/components/parameter-schema/form-mode/ParameterStringInput.vue @@ -1,9 +1,10 @@ <script setup lang="ts"> import { computed, watch, ref, onMounted, reactive } from "vue"; import { useBucketStore } from "@/stores/buckets"; -import { ObjectService } from "@/client/s3proxy"; +import { useS3ObjectStore } from "@/stores/s3objects"; const bucketRepository = useBucketStore(); +const s3objectRepository = useS3ObjectStore(); const props = defineProps({ parameter: { @@ -38,8 +39,6 @@ const s3Path = reactive<{ key: undefined, }); -const keysInBucket = ref<string[]>([]); - watch(defaultValue, (newVal, oldVal) => { if (newVal != oldVal && newVal != undefined) { emit("update:modelValue", newVal); @@ -72,13 +71,18 @@ const stringInput = ref<HTMLInputElement | undefined>(undefined); const format = computed<string | undefined>(() => props.parameter["format"]); const filesInBucket = computed<string[]>(() => - keysInBucket.value.filter((obj) => !obj.endsWith("/")), + (s3objectRepository.objectMapping[s3Path.bucket ?? ""] ?? []) + .filter((obj) => !obj.Key?.endsWith("/")) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .map((obj) => obj.Key!), ); const foldersInBucket = computed<string[]>(() => - keysInBucket.value + (s3objectRepository.objectMapping[s3Path.bucket ?? ""] ?? []) + .filter((obj) => obj.Key != undefined) .map((obj) => { - const parts = obj.split("/"); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const parts = obj.Key!.split("/"); return parts .slice(0, parts.length - 1) .map((part, index) => @@ -126,17 +130,14 @@ const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]); function updateKeysInBucket(bucketName?: string) { if (bucketName != null) { - ObjectService.objectGetBucketObjects(bucketName).then((objs) => { - keysInBucket.value = objs.map((obj) => obj.key); - }); - } else { - keysInBucket.value = []; + s3objectRepository.fetchS3Objects( + bucketName, + bucketRepository.ownPermissions[bucketName]?.file_prefix ?? undefined, + ); } } onMounted(() => { - bucketRepository.fetchBuckets(); - bucketRepository.fetchOwnPermissions(); if (format.value) { s3Path.key = defaultValue.value; } diff --git a/src/stores/buckets.ts b/src/stores/buckets.ts index c33eda777fa16e9ee958e48ca0ff60df044177a1..8389d86ceab16aa00201b6e4ecd36e42d900d682 100644 --- a/src/stores/buckets.ts +++ b/src/stores/buckets.ts @@ -209,6 +209,10 @@ export const useBucketStore = defineStore({ bucketName, uid, ).then(() => { + const userRepository = useAuthStore(); + if (uid == userRepository.currentUID) { + this.deleteOwnPermission(bucketName); + } if (this.bucketPermissionsMapping[bucketName] == undefined) { this.fetchBucketPermissions(bucketName); } else { diff --git a/src/stores/s3keys.ts b/src/stores/s3keys.ts index 7b6b1871a8aafc00e907c3132711fde54c164f13..915426f43a5128a7912be1ce316397d144c1a70b 100644 --- a/src/stores/s3keys.ts +++ b/src/stores/s3keys.ts @@ -2,6 +2,7 @@ import { defineStore } from "pinia"; import { useAuthStore } from "@/stores/users"; import type { S3Key } from "@/client/s3proxy"; import { S3KeyService } from "@/client/s3proxy"; +import { useS3ObjectStore } from "@/stores/s3objects"; export const useS3KeyStore = defineStore({ id: "s3keys", @@ -21,12 +22,14 @@ export const useS3KeyStore = defineStore({ }, }, actions: { - fetchS3Keys(uid: string, onFinally?: () => void): Promise<S3Key[]> { + fetchS3Keys(onFinally?: () => void): Promise<S3Key[]> { if (this.keys.length > 0) { onFinally?.(); } - return S3KeyService.s3KeyGetUserKeys(uid) + return S3KeyService.s3KeyGetUserKeys(useAuthStore().currentUID) .then((keys) => { + const s3ObjectRepository = useS3ObjectStore(); + s3ObjectRepository.updateS3Client(keys[0]); const newMapping: Record<string, S3Key> = {}; for (const key of keys) { newMapping[key.access_key] = key; @@ -42,7 +45,9 @@ export const useS3KeyStore = defineStore({ access_id, userRepository.currentUID, ).then(() => { + const s3ObjectRepository = useS3ObjectStore(); delete this.keyMapping[access_id]; + s3ObjectRepository.updateS3Client(this.keys[0]); }); }, createS3Key(): Promise<S3Key> { diff --git a/src/stores/s3objects.ts b/src/stores/s3objects.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2ed8e1fb2698b3e4ecfd3fca1745655d24c5221 --- /dev/null +++ b/src/stores/s3objects.ts @@ -0,0 +1,221 @@ +import { defineStore } from "pinia"; +import type { _Object as S3Object } from "@aws-sdk/client-s3"; +import { + CopyObjectCommand, + GetObjectCommand, + PutObjectCommand, + S3Client, +} from "@aws-sdk/client-s3"; +import { + paginateListObjectsV2, + DeleteObjectCommand, + DeleteObjectsCommand, +} from "@aws-sdk/client-s3"; +import { environment } from "@/environment"; +import type { S3Key } from "@/client/s3proxy"; +import { useBucketStore } from "@/stores/buckets"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import dayjs from "dayjs"; +import { Upload } from "@aws-sdk/lib-storage"; +import type { Progress } from "@aws-sdk/lib-storage"; + +export const useS3ObjectStore = defineStore({ + id: "s3objects", + state: () => + ({ + objectMapping: {}, + client: new S3Client({ + region: "us-east-1", + endpoint: environment.S3_URL, + forcePathStyle: true, + credentials: { + accessKeyId: "", + secretAccessKey: "", + }, + }), + }) as { + objectMapping: Record<string, S3Object[]>; + client: S3Client; + }, + getters: { + getPresignedUrl(): (bucketName: string, key: string) => Promise<string> { + return (bucketName, key) => { + const command = new GetObjectCommand({ + Bucket: bucketName, + Key: key, + }); + return getSignedUrl(this.client, command, { + expiresIn: 30, + }); + }; + }, + }, + actions: { + _pushObject(bucketName: string, newObj: S3Object) { + if (this.objectMapping[bucketName] == undefined) { + this.fetchS3Objects(bucketName); + } else { + const objIndex = this.objectMapping[bucketName].findIndex( + (obj) => obj.Key === newObj.Key, + ); + if (objIndex > -1) { + this.objectMapping[bucketName][objIndex] = newObj; + } else { + this.objectMapping[bucketName].push(newObj); + } + } + }, + updateS3Client(s3Key: S3Key) { + this.client = new S3Client({ + region: "us-east-1", + endpoint: environment.S3_URL, + forcePathStyle: true, + credentials: { + accessKeyId: s3Key.access_key, + secretAccessKey: s3Key.secret_key, + }, + }); + }, + async fetchS3Objects( + bucketName: string, + prefix?: string, + onFinally?: () => void, + ): Promise<S3Object[]> { + if (this.objectMapping[bucketName] != undefined) { + onFinally?.(); + } + const pag = paginateListObjectsV2( + { client: this.client }, + { Bucket: bucketName, Prefix: prefix }, + ); + const objs: S3Object[] = []; + try { + for await (const page of pag) { + objs.push(...(page.Contents ?? [])); + } + this.objectMapping[bucketName] = objs; + } finally { + onFinally?.(); + } + return objs; + }, + deleteObject(bucketName: string, key: string): Promise<void> { + const command = new DeleteObjectCommand({ + Bucket: bucketName, + Key: key, + }); + return this.client + .send(command) + .then(() => { + const bucketRepository = useBucketStore(); + bucketRepository.fetchBucket(bucketName); + if (this.objectMapping[bucketName] == undefined) { + this.fetchS3Objects(bucketName); + } else { + this.objectMapping[bucketName] = this.objectMapping[ + bucketName + ].filter((obj) => obj.Key !== key); + } + }) + .catch((err) => { + console.error(err); + }); + }, + deleteObjectsWithPrefix(bucketName: string, prefix: string): Promise<void> { + if (this.objectMapping[bucketName] == undefined) { + return Promise.resolve(); + } + const command = new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { + Objects: this.objectMapping[bucketName] + .filter((obj) => obj.Key?.startsWith(prefix)) + .map((obj) => { + return { Key: obj.Key }; + }), + }, + }); + return this.client.send(command).then(() => { + const bucketRepository = useBucketStore(); + bucketRepository.fetchBucket(bucketName); + if (this.objectMapping[bucketName] == undefined) { + this.fetchS3Objects(bucketName); + } else { + this.objectMapping[bucketName] = this.objectMapping[ + bucketName + ].filter((obj) => !obj.Key?.startsWith(prefix)); + } + }); + }, + copyObject( + srcBucket: string, + srcObject: S3Object, + destBucket: string, + destKey: string, + ): Promise<S3Object> { + if (srcObject.Key == undefined) { + return Promise.resolve({}); + } + const command = new CopyObjectCommand({ + Bucket: destBucket, + CopySource: encodeURI(`/${srcBucket}/${srcObject.Key}`), + Key: destKey, + }); + return this.client.send(command).then(() => { + const newObj = { + Key: destKey, + Size: srcObject.Size, + LastModified: dayjs().toDate(), + }; + this._pushObject(destBucket, newObj); + return newObj; + }); + }, + async uploadObjectFile( + bucketName: string, + key: string, + file: File, + onProgress?: (progress: Progress) => void, + ): Promise<S3Object> { + const parallelUploads3 = new Upload({ + client: this.client, + params: { + Bucket: bucketName, + Body: file, + ContentType: file.type, + Key: key, + }, + queueSize: 4, // optional concurrency configuration + leavePartsOnError: false, // optional manually handle dropped parts + }); + if (onProgress != undefined) { + parallelUploads3.on("httpUploadProgress", onProgress); + } + await parallelUploads3.done(); + const newObj = { + Key: key, + Size: file.size ?? 0, + LastModified: dayjs().toDate(), + }; + this._pushObject(bucketName, newObj); + return newObj; + }, + createFolder(bucketName: string, key: string): Promise<S3Object> { + const command = new PutObjectCommand({ + Bucket: bucketName, + Body: "", + ContentType: "text/plain", + Key: key, + }); + return this.client.send(command).then(() => { + const newObj = { + Key: key, + Size: 0, + LastModified: dayjs().toDate(), + }; + this._pushObject(bucketName, newObj); + return newObj; + }); + }, + }, +}); diff --git a/src/stores/users.ts b/src/stores/users.ts index 9a96af0cf942662ba4d22841e630f5bff364f7e4..76e987a733661dbacabfd58c096ad5bd7adacb6c 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -8,6 +8,7 @@ import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import { useBucketStore } from "@/stores/buckets"; import { useWorkflowStore } from "@/stores/workflows"; import { useS3KeyStore } from "@/stores/s3keys"; +import { useS3ObjectStore } from "@/stores/s3objects"; type DecodedToken = { exp: number; @@ -104,8 +105,6 @@ export const useAuthStore = defineStore({ }, updateUser(user: User) { this.user = user; - const keyRepository = useS3KeyStore(); - keyRepository.fetchS3Keys(user.uid); }, logout() { this.$reset(); @@ -113,6 +112,7 @@ export const useAuthStore = defineStore({ useBucketStore().$reset(); useWorkflowStore().$reset(); useS3KeyStore().$reset(); + useS3ObjectStore().$reset(); }, fetchUsernames(uids: string[]): Promise<User[]> { const filteredIds = uids diff --git a/src/types/PseudoFolder.ts b/src/types/PseudoFolder.ts index 1d363fe2c05b8daab9325cf2da6009d71e936d0f..c65e1f950c9a0825fae56ef479820bb9bff2ee4c 100644 --- a/src/types/PseudoFolder.ts +++ b/src/types/PseudoFolder.ts @@ -1,16 +1,16 @@ -import type { S3ObjectMetaInformation } from "@/client/s3proxy"; +import type { _Object as S3Object } from "@aws-sdk/client-s3"; -export interface S3ObjectWithFolder extends S3ObjectMetaInformation { +export interface S3ObjectWithFolder extends S3Object { folder: string[]; pseudoFileName: string; } export type S3PseudoFolder = { - size: number; + Size: number; parentFolder: string[]; - last_modified: string; + LastModified: Date; name: string; - key: string; + Key: string; }; export type FolderTree = { diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 20c717db83793b7a28918d699f8759f6acdc1173..2f0d18aec0d76b59123fa8501ec39cceee14cb50 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -64,7 +64,7 @@ onMounted(() => { class="img-fluid mb-3" width="128" height="128" - alt="..." + alt="CloWM Logo" /> <h1> <span class="blue fw-bold">Clo</span><span class="red fw-bold">W</span @@ -98,7 +98,7 @@ onMounted(() => { <img src="/src/assets/images/nfdi.svg" alt="NFDI4Microbiota Logo" - height="70" + height="50" /> </a> </div> @@ -108,25 +108,25 @@ onMounted(() => { <img src="/src/assets/images/denbi.svg" alt="de.NBI Logo" - height="70" + height="50" /> </a> </div> <div class="border rounded p-4 icon text-center"> <h4 class="mb-4">Hosted By</h4> <a href="https://bibi.uni-bielefeld.de/"> - <img src="/src/assets/images/bibi.png" alt="BiBi Logo" height="70" /> + <img src="/src/assets/images/bibi.png" alt="BiBi Logo" height="50" /> </a> </div> <div class="border rounded p-4 icon text-center"> <h4 class="mb-4">Funded By</h4> - <img src="/src/assets/images/dfg.png" alt="DFG Logo" height="70" /> + <img src="/src/assets/images/dfg.png" alt="DFG Logo" height="50" /> </div> <div class="border rounded p-4 icon text-center"> <img src="/src/assets/images/unibi.svg" alt="Bielefeld University Logo" - height="70" + height="50" /> </div> </div> diff --git a/src/views/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue index cafdf0596bb62b1d9e0ff6c90b41c07a9d242827..39d238f7e2f3aa0226e057458c84910d1776a563 100644 --- a/src/views/object-storage/BucketView.vue +++ b/src/views/object-storage/BucketView.vue @@ -1,15 +1,10 @@ <script setup lang="ts"> import { onMounted, reactive, watch, computed } from "vue"; -import type { - S3ObjectMetaInformation, - BucketPermissionOut, -} from "@/client/s3proxy"; import type { FolderTree, S3PseudoFolder, S3ObjectWithFolder, } from "@/types/PseudoFolder"; -import { ObjectService } from "@/client/s3proxy"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { filesize } from "filesize"; import dayjs from "dayjs"; @@ -21,34 +16,16 @@ import PermissionModal from "@/components/object-storage/modals/PermissionModal. import ObjectDetailModal from "@/components/object-storage/modals/ObjectDetailModal.vue"; import CreateFolderModal from "@/components/object-storage/modals/CreateFolderModal.vue"; import DeleteModal from "@/components/modals/DeleteModal.vue"; -import { - S3Client, - DeleteObjectCommand, - DeleteObjectsCommand, - GetObjectCommand, -} from "@aws-sdk/client-s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import type { _Object as S3Object } from "@aws-sdk/client-s3"; import { useAuthStore } from "@/stores/users"; import { useBucketStore } from "@/stores/buckets"; -import { environment } from "@/environment"; +import { useS3ObjectStore } from "@/stores/s3objects"; import { useS3KeyStore } from "@/stores/s3keys"; const authStore = useAuthStore(); const bucketRepository = useBucketStore(); -const keyRepository = useS3KeyStore(); - -const client = computed<S3Client>( - () => - new S3Client({ - region: "us-east-1", - endpoint: environment.S3_URL, - forcePathStyle: true, - credentials: { - accessKeyId: keyRepository.keys[0]?.access_key ?? "", - secretAccessKey: keyRepository.keys[0]?.secret_key ?? "", - }, - }), -); +const objectRepository = useS3ObjectStore(); +const s3KeyRepository = useS3KeyStore(); // Constants // ----------------------------------------------------------------------------- @@ -56,14 +33,14 @@ const client = computed<S3Client>( const props = defineProps<{ bucketName: string; subFolders: string[] | string; - permission?: BucketPermissionOut; }>(); + const randomIDSuffix = Math.random().toString(16).substring(2, 8); let successToast: Toast | null = null; +let refreshTimeout: NodeJS.Timeout | undefined = undefined; // Reactive State // ----------------------------------------------------------------------------- - const deleteObjectsState = reactive<{ deletedItem: string; potentialObjectToDelete: string; @@ -75,36 +52,28 @@ const deleteObjectsState = reactive<{ }); const objectState = reactive<{ - objects: S3ObjectMetaInformation[]; loading: boolean; filterString: string; bucketNotFoundError: boolean; bucketPermissionError: boolean; - createdPermission: undefined | BucketPermissionOut; editObjectKey: string; - copyObject: S3ObjectMetaInformation; - viewDetailObject: S3ObjectMetaInformation; + copyObject: S3Object; + viewDetailObject: S3Object; }>({ - objects: [], loading: true, filterString: "", bucketNotFoundError: false, bucketPermissionError: false, - createdPermission: undefined, editObjectKey: "", copyObject: { - key: "", - size: 0, - bucket: "", - last_modified: "2022-01-01", - content_type: "text/plain", + Key: "", + Size: 0, + LastModified: new Date(), }, viewDetailObject: { - key: "", - size: 0, - bucket: "", - last_modified: "2022-01-01", - content_type: "text/plain", + Key: "", + Size: 0, + LastModified: new Date(), }, }); @@ -113,13 +82,17 @@ const objectState = reactive<{ const filteredObjects = computed<(S3ObjectWithFolder | S3PseudoFolder)[]>( () => { return objectState.filterString.length > 0 - ? visibleObjects.value.filter((obj) => - obj.key.includes(objectState.filterString), + ? visibleObjects.value.filter( + (obj) => obj.Key?.includes(objectState.filterString), ) : visibleObjects.value; }, ); +const s3Objects = computed<S3Object[]>( + () => objectRepository.objectMapping[props.bucketName] ?? [], +); + const folderStructure = computed<FolderTree>(() => { /** * Store the entire folder structure in a bucket in a tree-like data structure @@ -170,8 +143,8 @@ const objectsWithFolders = computed<S3ObjectWithFolder[]>(() => { * folder: dir1, dir2 * filename: text.txt */ - return objectState.objects.map((obj) => { - const splittedKey = obj.key.split("/"); + return s3Objects.value.map((obj) => { + const splittedKey = obj.Key?.split("/") ?? [""]; return { ...obj, pseudoFileName: splittedKey[splittedKey.length - 1], @@ -216,17 +189,19 @@ const visibleObjects = computed<(S3ObjectWithFolder | S3PseudoFolder)[]>(() => { ); const folderLastModified = dayjs( calculateFolderLastModified(currentFolder.subFolders[subFolderName]), - ).toISOString(); + ).toDate(); return { name: subFolderName, - size: folderSize, - key: subFolderName, + Size: folderSize, + Key: subFolderName, parentFolder: currentSubFolders.value, - last_modified: folderLastModified, + LastModified: folderLastModified, } as S3PseudoFolder; }), ); - return arr.filter((obj) => !obj.key.endsWith("/") && obj.key.length > 0); + return arr.filter( + (obj) => !obj.Key?.endsWith("/") && (obj.Key?.length ?? 0) > 0, + ); }); const subFolderInUrl = computed<boolean>( @@ -235,9 +210,18 @@ const subFolderInUrl = computed<boolean>( const errorLoadingObjects = computed<boolean>( () => objectState.bucketPermissionError || objectState.bucketNotFoundError, ); -const writableBucket = computed<boolean>(() => - bucketRepository.writableBucket(props.bucketName), -); +const writableBucket = computed<boolean>(() => { + // Allow only upload in bucket folder with respect to permission prefix + let prefixWritable = true; + if ( + bucketRepository.ownPermissions[props.bucketName]?.file_prefix != undefined + ) { + prefixWritable = + bucketRepository.ownPermissions[props.bucketName]?.file_prefix === + currentSubFolders.value.join("/") + "/"; + } + return bucketRepository.writableBucket(props.bucketName) && prefixWritable; +}); const readableBucket = computed<boolean>(() => bucketRepository.readableBucket(props.bucketName), ); @@ -249,7 +233,9 @@ watch( (newBucketName, oldBucketName) => { if (oldBucketName !== newBucketName) { // If bucket is changed, update the objects - updateObjects(newBucketName); + objectState.bucketPermissionError = false; + objectState.bucketNotFoundError = false; + fetchObjects(); objectState.filterString = ""; } }, @@ -273,7 +259,17 @@ watch( // Lifecycle Hooks // ----------------------------------------------------------------------------- onMounted(() => { - updateObjects(props.bucketName); + let counter = 0; + const onFinally = () => { + counter++; + if (counter > 1) { + fetchObjects(); + } + }; + // wait till s3keys and ownPermissions are available before fetching objects + s3KeyRepository.fetchS3Keys(onFinally); + bucketRepository.fetchOwnPermissions(onFinally); + document .querySelectorAll(".tooltip-container") .forEach( @@ -291,7 +287,7 @@ onMounted(() => { */ function calculateFolderSize(folder: FolderTree): number { let folderSize = 0; - folderSize += folder.files.reduce((acc, file) => acc + file.size, 0); + folderSize += folder.files.reduce((acc, file) => acc + (file.Size ?? 0), 0); for (const subFolderName of Object.keys(folder.subFolders)) { folderSize += calculateFolderSize(folder.subFolders[subFolderName]); } @@ -306,7 +302,7 @@ function calculateFolderSize(folder: FolderTree): number { function calculateFolderLastModified(folder: FolderTree): string { let lastModified: dayjs.Dayjs; lastModified = folder.files - .map((f) => dayjs(f.last_modified)) + .map((f) => dayjs(f.LastModified)) .reduce( (acc, fileAccessed) => (fileAccessed.isAfter(acc) ? fileAccessed : acc), dayjs("2000-01-01"), @@ -323,63 +319,45 @@ function calculateFolderLastModified(folder: FolderTree): string { } /** - * Load the meta information about objects from a bucket - * @param bucketName Name of a bucket + * Fetch object from bucket with loading animation */ -function updateObjects(bucketName: string) { - objectState.bucketNotFoundError = false; - objectState.bucketPermissionError = false; +function fetchObjects() { objectState.loading = true; - ObjectService.objectGetBucketObjects(bucketName) - .then((objs) => { - objectState.objects = objs; + const prefix: string | undefined = + bucketRepository.ownPermissions[props.bucketName]?.file_prefix ?? undefined; + objectRepository + .fetchS3Objects(props.bucketName, prefix, () => { + objectState.loading = false; }) .catch((error) => { - if (error.status === 404) { - objectState.bucketNotFoundError = true; - } else if (error.status == 403) { + if (error.Code == "AccessDenied") { objectState.bucketPermissionError = true; + } else { + objectState.bucketNotFoundError = true; } - }) - .finally(() => { - objectState.loading = false; }); } +/** + * Fetch the meta information about objects from a bucket + */ +function refreshObjects() { + clearTimeout(refreshTimeout); + refreshTimeout = setTimeout(() => { + fetchObjects(); + }, 500); +} + function isS3Object( obj: S3PseudoFolder | S3ObjectWithFolder, ): obj is S3ObjectWithFolder { return (obj as S3ObjectWithFolder).folder !== undefined; } -/** - * callback function when an object has been uploaded - * @param newObject Uploaded object - */ -function objectUploaded(newObject: S3ObjectMetaInformation) { - bucketRepository.fetchBucket(newObject.bucket); - const index = objectState.objects.findIndex( - (obj) => obj.key === newObject.key, - ); - if (index > -1) { - objectState.objects[index] = newObject; - } else { - objectState.objects.push(newObject); +function deleteObject(key?: string) { + if (key == undefined) { + return; } -} - -/** - * callback function when an object has been copied - * @param copiedObject Uploaded object - */ -function objectCopied(copiedObject: S3ObjectMetaInformation) { - bucketRepository.fetchBucket(copiedObject.bucket); - if (copiedObject.bucket === props.bucketName) { - objectState.objects.push(copiedObject); - } -} - -function deleteObject(key: string) { deleteObjectsState.potentialObjectToDelete = key; deleteObjectsState.deleteFolder = false; } @@ -389,24 +367,11 @@ function deleteObject(key: string) { * @param key Key of the Object */ function confirmedDeleteObject(key: string) { - const command = new DeleteObjectCommand({ - Bucket: props.bucketName, - Key: key, + objectRepository.deleteObject(props.bucketName, key).then(() => { + const splittedKey = key.split("/"); + deleteObjectsState.deletedItem = splittedKey[splittedKey.length - 1]; + successToast?.show(); }); - client.value - .send(command) - .then(() => { - bucketRepository.fetchBucket(props.bucketName); - const splittedKey = key.split("/"); - deleteObjectsState.deletedItem = splittedKey[splittedKey.length - 1]; - successToast?.show(); - objectState.objects = objectState.objects.filter( - (obj) => obj.key !== key, - ); - }) - .catch((err) => { - console.error(err); - }); } /** @@ -414,12 +379,11 @@ function confirmedDeleteObject(key: string) { * @param key Key of the object * @param bucket Bucket of the object */ -async function downloadObject(key: string, bucket: string) { - const command = new GetObjectCommand({ - Bucket: bucket, - Key: key, - }); - const url = await getSignedUrl(client.value, command, { expiresIn: 30 }); +async function downloadObject(bucket: string, key?: string) { + if (key == undefined) { + return; + } + const url = await objectRepository.getPresignedUrl(bucket, key); //creating an invisible element const element = document.createElement("a"); element.setAttribute("href", url); @@ -439,29 +403,12 @@ function deleteFolder(folderPath: string) { * @param folderPath Path to the folder with a trailing "/", e.g. some/path/to/a/folder/ */ function confirmedDeleteFolder(folderPath: string) { - const command = new DeleteObjectsCommand({ - Bucket: props.bucketName, - Delete: { - Objects: objectState.objects - .filter((obj) => obj.key.startsWith(folderPath)) - .map((obj) => { - return { Key: obj.key }; - }), - }, - }); - client.value - .send(command) + objectRepository + .deleteObjectsWithPrefix(props.bucketName, folderPath) .then(() => { - bucketRepository.fetchBucket(props.bucketName); const splittedPath = folderPath.split("/"); deleteObjectsState.deletedItem = splittedPath[splittedPath.length - 2]; successToast?.show(); - objectState.objects = objectState.objects.filter( - (obj) => !obj.key.startsWith(folderPath), - ); - }) - .catch((err) => { - console.error(err); }); } @@ -541,9 +488,9 @@ function getObjectFileName(key: string): string { </nav> <!-- Inputs on top --> <!-- Search bucket text input --> - <div class="row"> - <div class="col-8"> - <div class="input-group mt-2 rounded shadow-sm"> + <div class="d-flex justify-content-between align-items-center"> + <div class="flex-grow-1 me-2"> + <div class="input-group rounded shadow-sm"> <span class="input-group-text" id="objects-search-wrapping" ><font-awesome-icon icon="fa-solid fa-magnifying-glass" /></span> @@ -559,7 +506,18 @@ function getObjectFileName(key: string): string { </div> </div> <!-- Upload object button --> - <div id="BucketViewButtons" class="col-auto"> + <div id="BucketViewButtons" class=""> + <button + type="button" + class="btn btn-light me-3 tooltip-container border shadow-sm" + :disabled="errorLoadingObjects" + data-bs-toggle="tooltip" + data-bs-title="Refresh Objects" + @click="refreshObjects" + > + <font-awesome-icon icon="fa-solid fa-arrow-rotate-right" /> + <span class="visually-hidden">Refresh Objects</span> + </button> <button type="button" class="btn btn-light me-2 tooltip-container border shadow-sm" @@ -573,16 +531,14 @@ function getObjectFileName(key: string): string { </button> <upload-object-modal :bucket-name="props.bucketName" - :s3-client="client" modalID="upload-object-modal" :key-prefix="currentSubFolders.join('/')" :edit-object-file-name="undefined" - @object-created="objectUploaded" /> <!-- Add folder button --> <button type="button" - class="btn btn-light m-2 tooltip-container border shadow-sm" + class="btn btn-light me-3 tooltip-container border shadow-sm" :disabled="errorLoadingObjects || !writableBucket" data-bs-toggle="modal" data-bs-title="Create Folder" @@ -594,17 +550,15 @@ function getObjectFileName(key: string): string { </button> <create-folder-modal :bucket-name="props.bucketName" - :s3-client="client" modalID="create-folder-modal" :key-prefix="currentSubFolders.join('/')" - @folder-created="objectUploaded" /> <!-- Add bucket permission button --> <button v-if="!authStore.foreignUser" :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)" type="button" - class="btn btn-light m-2 tooltip-container border shadow-sm" + class="btn btn-light me-2 tooltip-container border shadow-sm" :disabled="errorLoadingObjects" data-bs-toggle="modal" data-bs-title="Create Bucket Permission" @@ -628,7 +582,7 @@ function getObjectFileName(key: string): string { v-if="!authStore.foreignUser" :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)" type="button" - class="btn btn-light m-2 tooltip-container border shadow-sm" + class="btn btn-light tooltip-container border shadow-sm" :disabled="errorLoadingObjects" data-bs-title="List Bucket Permission" data-bs-toggle="modal" @@ -639,6 +593,7 @@ function getObjectFileName(key: string): string { </button> <permission-list-modal v-if=" + objectState.loading == false && bucketRepository.ownPermissions[props.bucketName] == undefined && !authStore.foreignUser " @@ -713,7 +668,7 @@ function getObjectFileName(key: string): string { </tbody> <!-- Table body when showing objects --> <tbody v-else> - <tr v-for="obj in filteredObjects" :key="obj.key"> + <tr v-for="obj in filteredObjects" :key="obj.Key"> <th scope="row" class="text-truncate"> <!-- Show file name if row is an object --> <div v-if="isS3Object(obj)">{{ obj.pseudoFileName }}</div> @@ -737,12 +692,14 @@ function getObjectFileName(key: string): string { class="date-tooltip" data-bs-toggle="tooltip" :data-bs-title=" - dayjs(obj.last_modified).format('DD.MM.YYYY HH:mm:ss') + dayjs(obj.LastModified).format('DD.MM.YYYY HH:mm:ss') " - >{{ dayjs(obj.last_modified).fromNow() }}</span + >{{ dayjs(obj.LastModified).fromNow() }}</span > </td> - <td>{{ filesize(obj.size, { base: 2, standard: "jedec" }) }}</td> + <td> + {{ filesize(obj.Size ?? 0, { base: 2, standard: "jedec" }) }} + </td> <!-- Show buttons with dropdown menu if row is an object --> <td class="text-end"> <div @@ -753,7 +710,7 @@ function getObjectFileName(key: string): string { <button type="button" class="btn btn-secondary" - @click="downloadObject(obj.key, props.bucketName)" + @click="downloadObject(props.bucketName, obj.Key)" :disabled="!readableBucket" > Download @@ -786,7 +743,7 @@ function getObjectFileName(key: string): string { :disabled="!writableBucket" data-bs-toggle="modal" data-bs-target="#edit-object-modal" - @click="objectState.editObjectKey = obj.key" + @click="objectState.editObjectKey = obj.Key ?? ''" > Edit </button> @@ -807,7 +764,7 @@ function getObjectFileName(key: string): string { <button class="dropdown-item text-danger align-middle" type="button" - @click="deleteObject(obj.key)" + @click="deleteObject(obj.Key)" data-bs-toggle="modal" data-bs-target="#delete-object-modal" :disabled="!writableBucket" @@ -842,19 +799,17 @@ function getObjectFileName(key: string): string { </table> <upload-object-modal :bucket-name="props.bucketName" - :s3-client="client" modalID="edit-object-modal" :key-prefix="currentSubFolders.join('/')" :edit-object-file-name="getObjectFileName(objectState.editObjectKey)" - @object-created="objectUploaded" /> <copy-object-modal - :source-object="objectState.copyObject" - :s3-client="client" + :src-object="objectState.copyObject" + :src-bucket="bucketName" modalID="copy-object-modal" - @object-copied="objectCopied" /> <object-detail-modal + :bucket="bucketName" :s3-object="objectState.viewDetailObject" modalID="detail-object-modal" /> diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue index a9072b88e90fbb975502e71637ef1312eb2788eb..ffade6583732a910ce744c1a132d472383203c85 100644 --- a/src/views/object-storage/S3KeysView.vue +++ b/src/views/object-storage/S3KeysView.vue @@ -26,7 +26,7 @@ const allowKeyDeletion = computed<boolean>(() => keyRepository.keys.length > 1); function fetchKeys() { keyRepository - .fetchS3Keys(authStore.currentUID, () => (keyState.initialLoading = false)) + .fetchS3Keys(() => (keyState.initialLoading = false)) .then((keys) => { if (keyState.activeKey >= keys.length) { keyState.activeKey = keys.length - 1; diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue index 51445454d9bae200ba7c345d32e0d9ec6186d3a4..691068cdcbebb185d4a77036c4e4016d984c738d 100644 --- a/src/views/workflows/ListWorkflowExecutionsView.vue +++ b/src/views/workflows/ListWorkflowExecutionsView.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; -import { onMounted, reactive, computed } from "vue"; +import { onMounted, reactive, computed, onUnmounted } from "vue"; import type { WorkflowExecutionOut } from "@/client/workflow"; import { WorkflowExecutionStatus } from "@/client/workflow"; import dayjs from "dayjs"; @@ -13,6 +13,7 @@ const workflowRepository = useWorkflowStore(); const executionRepository = useWorkflowExecutionStore(); let refreshTimeout: NodeJS.Timeout | undefined = undefined; +let intervalId: NodeJS.Timer | undefined = undefined; const executionsState = reactive<{ loading: boolean; @@ -115,11 +116,26 @@ function cancelWorkflowExecution(executionId: string) { executionRepository.cancelExecution(executionId); } +function refreshRunningWorkflowExecution() { + Promise.all( + executionRepository.executions + .filter((execution) => workflowExecutionCancelable(execution.status)) + .map((execution) => + executionRepository.fetchExecution(execution.execution_id), + ), + ); +} + onMounted(() => { workflowRepository.fetchWorkflows(); updateExecutions(); + intervalId = setInterval(refreshRunningWorkflowExecution, 5000); new Tooltip("#refreshExecutionsButton"); }); + +onUnmounted(() => { + clearInterval(intervalId); +}); </script> <template> diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue index ceea0bad90b512ee882398a19e26bb781f628217..4b668301b2a23a4157d19d3d9fcbfc84870fe3f7 100644 --- a/src/views/workflows/WorkflowView.vue +++ b/src/views/workflows/WorkflowView.vue @@ -255,7 +255,7 @@ onMounted(() => { v-if="activeVersionModeIds.length > 0" class="row align-items-center mb-3 fs-5" > - <label class="col-sm-1 col-form-label">Mode:</label> + <label class="col-sm-1 col-form-label"><b>Mode:</b></label> <div class="col-sm-11"> <select class="form-select w-fit" v-model="workflowState.activeModeId"> <option