diff --git a/package.json b/package.json index c0a481bbf247064dd731a6e9d9d4fdc1e9902a13..71a2838dd39db095e4ec49c87480e2db8a991a55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxyapi-ui", - "version": "1.0.0", + "version": "1.0.1", "scripts": { "dev": "vite", "build": "run-p type-check build-only", diff --git a/src/components/BucketListItem.vue b/src/components/BucketListItem.vue index 86ca530cdd58f5ed1fbba8c8ef7b4a020c7ad3f1..536d121408a892731e237e7956b50cd949fe063c 100644 --- a/src/components/BucketListItem.vue +++ b/src/components/BucketListItem.vue @@ -1,27 +1,43 @@ <script setup lang="ts"> -import type { BucketOut, BucketPermissionOut } from "@/client"; +import type { + BucketOut, + BucketPermissionIn, + BucketPermissionOut, +} from "@/client"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; import PermissionModal from "@/components/Modals/PermissionModal.vue"; import BucketDetailModal from "@/components/Modals/BucketDetailModal.vue"; import dayjs from "dayjs"; import { filesize } from "filesize"; -import { onMounted } from "vue"; +import { computed, onMounted } from "vue"; +import type { ComputedRef } from "vue"; import { Tooltip } from "bootstrap"; +import { useBucketStore } from "@/stores/buckets"; +import { useRouter } from "vue-router"; const props = defineProps<{ active: boolean; bucket: BucketOut; loading: boolean; - permission: BucketPermissionOut | undefined; }>(); const randomIDSuffix = Math.random().toString(16).substr(2, 8); +const permissionRepository = useBucketStore(); +const router = useRouter(); + +const permission: ComputedRef<BucketPermissionOut | undefined> = computed(() => + permissionRepository.getBucketPermission(props.bucket.name) +); const emit = defineEmits<{ (e: "delete-bucket", bucketName: string): void; - (e: "permission-deleted", bucketName: string): void; }>(); +function permissionDeleted(perm: BucketPermissionIn) { + permissionRepository.deleteOwnPermission(perm.bucket_name); + router.push({ name: "buckets" }); +} + onMounted(() => { if (!props.loading) { new Tooltip("#tooltip-" + randomIDSuffix); @@ -31,22 +47,22 @@ onMounted(() => { <template> <permission-modal - v-if="props.permission != null && props.active" + v-if="permission != null && props.active" :modalID="'view-permission-modal' + randomIDSuffix" :bucket-name="props.bucket.name" :sub-folders="{ subFolders: {}, files: [] }" - :edit-user-permission="props.permission" + :edit-user-permission="permission" :readonly="true" :editable="false" :deletable="true" :back-modal-id="undefined" - @permission-deleted="(perm) => emit('permission-deleted', perm.bucket_name)" + @permission-deleted="permissionDeleted" /> <bucket-detail-modal v-if="props.active" :modalID="'view-bucket-details-modal' + randomIDSuffix" :bucket="props.bucket" - :edit-user-permission="props.permission" + :edit-user-permission="permission" /> <div class="mt-2 mb-2"> <div @@ -72,7 +88,7 @@ onMounted(() => { {{ bucket.name }} <div> <bootstrap-icon - v-if="props.active && props.permission == null" + v-if="props.active && permission == null" icon="trash-fill" class="delete-icon me-2" :width="16" @@ -96,12 +112,12 @@ onMounted(() => { :hidden="!props.active" class="ps-2 pe-2 rounded-bottom bg-light text-bg-light border border-3 border-top-0 border-primary" > - <div v-if="props.permission != null" class="ms-1 pt-1 text-info"> + <div v-if="permission != null" class="ms-1 pt-1 text-info"> Foreign Bucket </div> <table class="table table-sm table-borderless mb-0"> <tbody> - <tr v-if="props.permission != null"> + <tr v-if="permission != null"> <th scope="row" class="fw-bold">Permission</th> <td> <a diff --git a/src/components/BucketView.vue b/src/components/BucketView.vue index 47aeec77f25bb46e3b0b18a76ed678c21f2e0e62..f176536ae7036de7a8d5cfc78e65b5a053013a4b 100644 --- a/src/components/BucketView.vue +++ b/src/components/BucketView.vue @@ -1,11 +1,7 @@ <script setup lang="ts"> import { onMounted, reactive, watch, computed } from "vue"; import type { ComputedRef } from "vue"; -import type { - S3ObjectMetaInformation, - BucketPermissionOut, - BucketOut, -} from "@/client"; +import type { S3ObjectMetaInformation, BucketPermissionOut } from "@/client"; import type { FolderTree, S3PseudoFolder, @@ -32,8 +28,10 @@ import { import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { awsAuthMiddlewareOptions } from "@aws-sdk/middleware-signing"; import { useAuthStore } from "@/stores/auth"; +import { useBucketStore } from "@/stores/buckets"; const authStore = useAuthStore(); +const bucketRepository = useBucketStore(); const middleware = [ // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -90,7 +88,6 @@ const props = defineProps<{ bucketName: string; subFolders: string[] | string; permission: BucketPermissionOut | undefined; - writableBuckets: BucketOut[]; }>(); const randomIDSuffix = Math.random().toString(16).substr(2, 8); let successToast: Toast | null = null; @@ -150,6 +147,7 @@ watch( if (oldBucketName !== newBucketName) { // If bucket is changed, update the objects updateObjects(newBucketName); + objectState.filterString = ""; } } ); @@ -281,10 +279,8 @@ const subFolderInUrl: ComputedRef<boolean> = computed( const errorLoadingObjects: ComputedRef<boolean> = computed( () => objectState.bucketPermissionError || objectState.bucketNotFoundError ); - -const writeS3Permission: ComputedRef<boolean> = computed( - () => - props.permission == undefined || props.permission.permission == "READWRITE" +const writableBucket: ComputedRef<boolean> = computed(() => + bucketRepository.writableBucket(props.bucketName) ); // Lifecycle Hooks @@ -374,9 +370,10 @@ function isS3Object( * @param newObject Uploaded object */ function objectUploaded(newObject: S3ObjectMetaInformation) { - const index = objectState.objects - .map((obj) => obj.key) - .indexOf(newObject.key); + bucketRepository.fetchBucket(newObject.bucket); + const index = objectState.objects.findIndex( + (obj) => obj.key === newObject.key + ); if (index > -1) { objectState.objects[index] = newObject; } else { @@ -389,6 +386,7 @@ function objectUploaded(newObject: S3ObjectMetaInformation) { * @param copiedObject Uploaded object */ function objectCopied(copiedObject: S3ObjectMetaInformation) { + bucketRepository.fetchBucket(copiedObject.bucket); if (copiedObject.bucket === props.bucketName) { objectState.objects.push(copiedObject); } @@ -411,6 +409,7 @@ function confirmedDeleteObject(key: string) { client .send(command) .then(() => { + bucketRepository.fetchBucket(props.bucketName); const splittedKey = key.split("/"); deleteObjectsState.deletedItem = splittedKey[splittedKey.length - 1]; successToast?.show(); @@ -466,6 +465,7 @@ function confirmedDeleteFolder(folderPath: string) { client .send(command) .then(() => { + bucketRepository.fetchBucket(props.bucketName); const splittedPath = folderPath.split("/"); deleteObjectsState.deletedItem = splittedPath[splittedPath.length - 2]; successToast?.show(); @@ -581,6 +581,7 @@ watch( placeholder="Search Objects" aria-label="Search Objects" aria-describedby="objects-search-wrapping" + :disabled="errorLoadingObjects" v-model.trim="objectState.filterString" /> </div> @@ -590,7 +591,7 @@ watch( <button type="button" class="btn btn-secondary me-2 tooltip-container" - :disabled="errorLoadingObjects || !writeS3Permission" + :disabled="errorLoadingObjects || !writableBucket" data-bs-toggle="modal" data-bs-title="Upload Object" data-bs-target="#upload-object-modal" @@ -610,7 +611,7 @@ watch( <button type="button" class="btn btn-secondary m-2 tooltip-container" - :disabled="errorLoadingObjects || !writeS3Permission" + :disabled="errorLoadingObjects || !writableBucket" data-bs-toggle="modal" data-bs-title="Create Folder" data-bs-target="#create-folder-modal" @@ -628,7 +629,7 @@ watch( /> <!-- Add bucket permission button --> <button - :hidden="props.permission != null" + :hidden="bucketRepository.getBucketPermission(props.bucketName) != null" type="button" class="btn btn-secondary m-2 tooltip-container" :disabled="errorLoadingObjects" @@ -658,7 +659,7 @@ watch( " /> <button - :hidden="props.permission != null" + :hidden="bucketRepository.getBucketPermission(props.bucketName) != null" type="button" class="btn btn-secondary m-2 tooltip-container" :disabled="errorLoadingObjects" @@ -675,7 +676,7 @@ watch( <span class="visually-hidden">View Bucket Permissions</span> </button> <permission-list-modal - v-if="props.permission == null" + v-if="bucketRepository.getBucketPermission(props.bucketName) == null" :bucket-name="props.bucketName" :sub-folders="folderStructure" modalID="permission-list-modal" @@ -702,7 +703,7 @@ watch( <caption> Displaying {{ - objectState.loading ? 0 : visibleObjects.length + objectState.loading ? 0 : filteredObjects.length }} Objects </caption> @@ -804,7 +805,7 @@ watch( <button class="dropdown-item" type="button" - :disabled="!writeS3Permission" + :disabled="!writableBucket" data-bs-toggle="modal" data-bs-target="#edit-object-modal" @click="objectState.editObjectKey = obj.key" @@ -816,7 +817,7 @@ watch( <button class="dropdown-item" type="button" - :disabled="!writeS3Permission" + :disabled="!writableBucket" data-bs-toggle="modal" data-bs-target="#copy-object-modal" @click="objectState.copyObject = obj" @@ -831,7 +832,7 @@ watch( @click="deleteObject(obj.key)" data-bs-toggle="modal" data-bs-target="#delete-object-modal" - :disabled="!writeS3Permission" + :disabled="!writableBucket" > <bootstrap-icon icon="trash-fill" @@ -850,7 +851,7 @@ watch( <button type="button" class="btn btn-danger btn-sm align-middle" - :disabled="!writeS3Permission" + :disabled="!writableBucket" data-bs-toggle="modal" data-bs-target="#delete-object-modal" @click=" @@ -885,7 +886,6 @@ watch( :source-object="objectState.copyObject" :s3-client="client" modalID="copy-object-modal" - :available-buckets="props.writableBuckets" @object-copied="objectCopied" /> <object-detail-modal diff --git a/src/components/Modals/CopyObjectModal.vue b/src/components/Modals/CopyObjectModal.vue index b4c72a29e865a259515a5b2584cc6189217d4f55..f3045c0b372024a144b37ed4b3a4ad72f06bdf2f 100644 --- a/src/components/Modals/CopyObjectModal.vue +++ b/src/components/Modals/CopyObjectModal.vue @@ -2,17 +2,16 @@ import type { S3Client } from "@aws-sdk/client-s3"; import { CopyObjectCommand } from "@aws-sdk/client-s3"; import BootstrapModal from "@/components/Modals/BootstrapModal.vue"; -import type { BucketOut } from "@/client"; import { Modal, Toast } from "bootstrap"; import { onMounted, reactive, watch } from "vue"; import type { S3ObjectMetaInformation } from "@/client"; import dayjs from "dayjs"; +import { useBucketStore } from "@/stores/buckets"; const props = defineProps<{ modalID: string; sourceObject: S3ObjectMetaInformation; s3Client: S3Client; - availableBuckets: BucketOut[]; }>(); const formState = reactive({ @@ -24,6 +23,7 @@ const formState = reactive({ destBucket: string; uploading: boolean; }); +const bucketRepository = useBucketStore(); const emit = defineEmits<{ (e: "object-copied", object: S3ObjectMetaInformation): void; @@ -165,7 +165,7 @@ onMounted(() => { > <option disabled selected>Select one...</option> <option - v-for="bucket in props.availableBuckets" + v-for="bucket in bucketRepository.writableBuckets" :key="bucket.name" :value="bucket.name" > diff --git a/src/components/Modals/CreateBucketModal.vue b/src/components/Modals/CreateBucketModal.vue index 2e9dec8d66ebca90909dbf14bc5d66e0ff40848d..f1447d9d68db7f577908c7301a5731515692027e 100644 --- a/src/components/Modals/CreateBucketModal.vue +++ b/src/components/Modals/CreateBucketModal.vue @@ -1,13 +1,13 @@ <script setup lang="ts"> -import { BucketService } from "@/client"; import type { BucketIn } from "@/client"; import { reactive, onMounted } from "vue"; import BootstrapModal from "@/components/Modals/BootstrapModal.vue"; import { useRouter } from "vue-router"; import { Modal } from "bootstrap"; +import { useBucketStore } from "@/stores/buckets"; const router = useRouter(); -const emit = defineEmits(["bucketCreated"]); +const bucketRepository = useBucketStore(); const bucket = reactive({ name: "", description: "" } as BucketIn); const formState = reactive({ validated: false, @@ -38,9 +38,9 @@ function createBucket() { bucket.name = bucket.name.trim(); if (form.checkValidity()) { formState.loading = true; - BucketService.bucketCreateBucket(bucket) - .then((createdBucket) => { - emit("bucketCreated", createdBucket); + bucketRepository.createBucket( + bucket, + (createdBucket) => { createBucketModal?.hide(); bucket.name = ""; bucket.description = ""; @@ -50,18 +50,19 @@ function createBucket() { name: "bucket", params: { bucketName: createdBucket.name, subFolders: [] }, }); - }) - .catch((error) => { + }, + (error) => { if ( error.status === 400 && error.body["detail"] === "Bucket name is already taken" ) { formState.bucketNameTaken = true; } - }) - .finally(() => { + }, + () => { formState.loading = false; - }); + } + ); } } diff --git a/src/stores/buckets.ts b/src/stores/buckets.ts new file mode 100644 index 0000000000000000000000000000000000000000..3172c713118d14930cac39486558398140c34edf --- /dev/null +++ b/src/stores/buckets.ts @@ -0,0 +1,141 @@ +import { defineStore } from "pinia"; +import { BucketPermissionsService, BucketService } from "@/client"; +import type { BucketOut, BucketIn, BucketPermissionOut } from "@/client"; +import { useAuthStore } from "@/stores/auth"; + +export const useBucketStore = defineStore({ + id: "buckets", + state: () => + ({ + buckets: [], + ownPermissions: {}, + } as { + buckets: BucketOut[]; + ownPermissions: Record<string, BucketPermissionOut>; + }), + getters: { + writableBuckets(): BucketOut[] { + return this.buckets.filter( + (bucket) => + this.ownPermissions[bucket.name] === undefined || + this.ownPermissions[bucket.name].permission !== "READ" + ); + }, + getBucketPermission(): ( + bucketName: string + ) => BucketPermissionOut | undefined { + return (bucketName) => this.ownPermissions[bucketName]; + }, + writableBucket(): (bucketName: string) => boolean { + return (bucketName) => + this.ownPermissions[bucketName] === undefined || + this.ownPermissions[bucketName].permission !== "READ"; + }, + }, + actions: { + _fetchOwnPermissions( + onFulfilled: + | ((buckets: BucketPermissionOut[]) => void) + | null + | undefined = null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onRejected: ((reason: any) => void) | null | undefined = null, + onFinally: (() => void) | null | undefined = null + ) { + const authStore = useAuthStore(); + if (authStore.user != null) { + BucketPermissionsService.bucketPermissionsListPermissionsPerUser( + authStore.user.uid + ) + .then((permissions) => { + const new_permissions: Record<string, BucketPermissionOut> = {}; + for (const perm of permissions) { + new_permissions[perm.bucket_name] = perm; + } + this.ownPermissions = new_permissions; + onFulfilled?.(permissions); + }) + .catch(onRejected) + .finally(onFinally); + } + }, + deleteOwnPermission(bucketName: string) { + this.buckets = this.buckets.filter( + (bucket) => bucket.name !== bucketName + ); + delete this.ownPermissions[bucketName]; + }, + fetchBuckets( + onFulfilled: ((buckets: BucketOut[]) => void) | null | undefined = null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onRejected: ((reason: any) => void) | null | undefined = null, + onFinally: (() => void) | null | undefined = null + ) { + BucketService.bucketListBuckets() + .then((buckets) => { + this.buckets = buckets; + onFulfilled?.(buckets); + }) + .catch(onRejected) + .finally(onFinally); + this._fetchOwnPermissions(); + }, + fetchBucket( + bucketName: string, + onFulfilled: ((bucket: BucketOut) => void) | null | undefined = null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onRejected: ((reason: any) => void) | null | undefined = null, + onFinally: (() => void) | null | undefined = null + ) { + BucketService.bucketGetBucket(bucketName) + .then((bucket) => { + this.buckets[ + this.buckets.findIndex( + (tempBucket) => tempBucket.name === bucket.name + ) + ] = bucket; + onFulfilled?.(bucket); + }) + .catch(onRejected) + .finally(onFinally); + }, + deleteBucket( + bucketName: string, + onFulfilled: (() => void) | null | undefined = null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onRejected: ((reason: any) => void) | null | undefined = null, + onFinally: (() => void) | null | undefined = null + ) { + BucketService.bucketDeleteBucket(bucketName, true) + .then(() => { + this.buckets = this.buckets.filter( + (bucket) => bucket.name !== bucketName + ); + onFulfilled?.(); + }) + .catch(onRejected) + .finally(onFinally); + }, + createBucket( + bucket: BucketIn, + onFulfilled: + | ((createdBucket: BucketOut) => void) + | null + | undefined = null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onRejected: ((reason: any) => void) | null | undefined = null, + onFinally: (() => void) | null | undefined = null + ) { + BucketService.bucketCreateBucket(bucket) + .then((createdBucket) => { + this.buckets.push(createdBucket); + this.buckets.sort((bucketA, bucketB) => + bucketA.name >= bucketB.name ? 1 : -1 + ); + onFulfilled?.(createdBucket); + }) + .catch(onRejected) + .finally(onFinally); + }, + }, +}); diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue index 572e517740c4dfed915e2292e95ee9ecc22907d3..082a51bd7cbf2d87489df5ff28c3b25664abc6fe 100644 --- a/src/views/object-storage/BucketsView.vue +++ b/src/views/object-storage/BucketsView.vue @@ -1,102 +1,53 @@ <script setup lang="ts"> import type { ComputedRef } from "vue"; import { computed, onMounted, reactive } from "vue"; -import type { BucketOut, BucketPermissionOut } from "@/client"; -import { BucketPermissionsService, BucketService } from "@/client"; +import type { BucketOut } from "@/client"; import { useRoute, useRouter } from "vue-router"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; import CreateBucketModal from "@/components/Modals/CreateBucketModal.vue"; import DeleteModal from "@/components/Modals/DeleteModal.vue"; import BucketListItem from "@/components/BucketListItem.vue"; -import { useAuthStore } from "@/stores/auth"; +import { useBucketStore } from "@/stores/buckets"; import { Modal } from "bootstrap"; const route = useRoute(); const router = useRouter(); -const authStore = useAuthStore(); +const bucketRepository = useBucketStore(); const bucketsState = reactive({ - buckets: [], - permissions: {}, filterString: "", potentialDeleteBucketName: "", loading: true, } as { - buckets: BucketOut[]; loading: boolean; filterString: string; - permissions: Record<string, BucketPermissionOut>; potentialDeleteBucketName: string; }); let deleteModal: Modal | null = null; function fetchBuckets() { - BucketService.bucketListBuckets() - .then((buckets) => { - bucketsState.buckets = buckets; - }) - .finally(() => { - bucketsState.loading = false; - }); - if (authStore.user != null) { - BucketPermissionsService.bucketPermissionsListPermissionsPerUser( - authStore.user.uid - ).then((permissions) => { - const new_permissions: Record<string, BucketPermissionOut> = {}; - for (const perm of permissions) { - new_permissions[perm.bucket_name] = perm; - } - bucketsState.permissions = new_permissions; - }); - } -} - -const writableBuckets: ComputedRef<BucketOut[]> = computed(() => { - return bucketsState.buckets.filter( - (bucket) => - bucketsState.permissions[bucket.name] === undefined || - bucketsState.permissions[bucket.name].permission !== "READ" - ); -}); - -const currentPermission: ComputedRef<BucketPermissionOut | undefined> = - computed(() => { - return bucketsState.permissions[route.params.bucketName as string]; + bucketRepository.fetchBuckets(null, null, () => { + bucketsState.loading = false; }); +} const filteredBuckets: ComputedRef<BucketOut[]> = computed(() => { return bucketsState.filterString.length > 0 - ? bucketsState.buckets.filter((bucket) => + ? bucketRepository.buckets.filter((bucket) => bucket.name.includes(bucketsState.filterString) ) - : bucketsState.buckets; + : bucketRepository.buckets; }); -function addBucket(bucket: BucketOut) { - bucketsState.buckets.push(bucket); - bucketsState.buckets.sort((bucketA, bucketB) => - bucketA.name >= bucketB.name ? 1 : -1 - ); -} - function deleteBucket(bucketName: string) { bucketsState.potentialDeleteBucketName = bucketName; deleteModal?.show(); } function confirmedDeleteBucket(bucketName: string) { - BucketService.bucketDeleteBucket(bucketName, true).then(() => { - bucketDeleted(bucketName); - }); -} - -function bucketDeleted(bucketName: string) { - if (bucketsState.buckets.map((bucket) => bucket.name).includes(bucketName)) { + bucketRepository.deleteBucket(bucketName, () => { router.push({ name: "buckets" }); - } - bucketsState.buckets = bucketsState.buckets.filter( - (bucket) => bucket.name !== bucketName - ); + }); } onMounted(() => { @@ -114,6 +65,7 @@ onMounted(() => { confirmedDeleteBucket(bucketsState.potentialDeleteBucketName) " /> + <CreateBucketModal modalID="create-bucket-modal" /> <div class="row m-2 border-bottom border-light mt-4"> <div class="col-12"></div> <h1 class="mb-2 text-light">Buckets</h1> @@ -139,10 +91,6 @@ onMounted(() => { <span class="visually-hidden">Create Bucket</span> </button> </div> - <create-bucket-modal - modalID="create-bucket-modal" - @bucket-created="addBucket" - /> <div class="input-group flex-nowrap mt-2"> <span class="input-group-text" id="buckets-search-wrapping" ><bootstrap-icon icon="search" :width="16" :height="16" @@ -169,9 +117,7 @@ onMounted(() => { " :bucket="bucket" :loading="false" - :permission="bucketsState.permissions[bucket.name]" @delete-bucket="deleteBucket" - @permission-deleted="(bucketName) => bucketDeleted(bucketName)" /> </div> <div v-else class="text-center fs-2 mt-5"> @@ -182,7 +128,8 @@ onMounted(() => { :height="56" style="color: var(--bs-secondary)" fill="currentColor" - /><br /> + /> + <br /> Could not find any Buckets </div> </div> @@ -206,10 +153,7 @@ onMounted(() => { </div> </div> <div class="col-9"> - <router-view - :permission="currentPermission" - :writable-buckets="writableBuckets" - ></router-view> + <router-view></router-view> </div> </div> </template>