diff --git a/frontend/src/assets/main.scss b/frontend/src/assets/main.scss index 3f32eeb0..65cb28a5 100644 --- a/frontend/src/assets/main.scss +++ b/frontend/src/assets/main.scss @@ -357,3 +357,8 @@ a:visited { a.p-tabview-header-action { text-decoration: none; } + +/* forms */ +.field span[role="alert"] { + color: $bcbox-error; +} diff --git a/frontend/src/assets/variables.scss b/frontend/src/assets/variables.scss index a7c46cdb..ce744318 100644 --- a/frontend/src/assets/variables.scss +++ b/frontend/src/assets/variables.scss @@ -11,6 +11,7 @@ $bcbox-primary: #036; $bcbox-link-text: #1a5a96; $bcbox-link-text-hover: #00f; $bcbox-outline-on-primary: #fff; +$bcbox-error: #d8292f; // highlighted sections, table rows $bcbox-highlight-background: #d9e1e8; diff --git a/frontend/src/components/bucket/BucketChildConfig.vue b/frontend/src/components/bucket/BucketChildConfig.vue new file mode 100644 index 00000000..cac14e66 --- /dev/null +++ b/frontend/src/components/bucket/BucketChildConfig.vue @@ -0,0 +1,129 @@ + + + diff --git a/frontend/src/components/bucket/BucketConfigForm.vue b/frontend/src/components/bucket/BucketConfigForm.vue index 365a888a..353b9888 100644 --- a/frontend/src/components/bucket/BucketConfigForm.vue +++ b/frontend/src/components/bucket/BucketConfigForm.vue @@ -86,7 +86,6 @@ const onSubmit = async (values: any) => { await bucketStore.fetchBuckets({ userId: getUserId.value, objectPerms: true }); // trim trailing "//", if present - // (modifying getBucketPath() instead seems to break nested bucket display [PR-146]) const currBucketPath = getBucketPath(initialValues as Bucket).endsWith('//') ? getBucketPath(initialValues as Bucket).slice(0, -1) : getBucketPath(initialValues as Bucket); diff --git a/frontend/src/components/bucket/BucketTable.vue b/frontend/src/components/bucket/BucketTable.vue index c481e64e..768af516 100644 --- a/frontend/src/components/bucket/BucketTable.vue +++ b/frontend/src/components/bucket/BucketTable.vue @@ -2,7 +2,7 @@ import { storeToRefs } from 'pinia'; import { ref, watch } from 'vue'; -import { BucketPermission, BucketTableBucketName } from '@/components/bucket'; +import { BucketChildConfig, BucketPermission, BucketTableBucketName } from '@/components/bucket'; import { Spinner } from '@/components/layout'; import { SyncButton } from '@/components/common'; import { Button, Column, Dialog, TreeTable, useConfirm } from '@/lib/primevue'; @@ -67,13 +67,6 @@ async function deleteBucket(bucketId: string) { await bucketStore.fetchBuckets({ userId: getUserId.value, objectPerms: true }); } -/** Get the full path to the first part of its key */ -function getFirstKeyPartPath(node: BucketTreeNode): string { - const parts = node.data.key.split(DELIMITER).filter((part) => part); - - return `${node.data.endpoint}/${node.data.bucket}/${parts[0]}`; -} - /** * Finds the nearest indirect path to node \ * Assumes the endpointMap paths have been pre sorted @@ -197,8 +190,7 @@ watch(getBuckets, () => { } else { if (node.data.key !== '/') { // Top level bucket not at root so create dummy hierarchy to reach it - const rootFullPath = getFirstKeyPartPath(node); - const rootKey = node.data.key.split(DELIMITER).filter((part) => part)[0]; + const rootFullPath = `${node.data.endpoint}/${node.data.bucket}//`; const dummyRootNode: BucketTreeNode = { key: rootFullPath, data: { @@ -206,10 +198,10 @@ watch(getBuckets, () => { active: false, bucket: node.data.bucket, bucketId: '', - bucketName: rootKey, + bucketName: node.data.bucket, dummy: true, endpoint: node.data.endpoint, - key: rootKey, + key: '/', region: '', secretAccessKey: '' }, @@ -296,10 +288,14 @@ watch(getBuckets, () => { header="Actions" header-class="text-right" body-class="action-buttons" - style="width: 250px" + style="width: 280px" > - - diff --git a/frontend/src/components/bucket/index.ts b/frontend/src/components/bucket/index.ts index 9dda7bcd..04257c05 100644 --- a/frontend/src/components/bucket/index.ts +++ b/frontend/src/components/bucket/index.ts @@ -1,3 +1,4 @@ +export { default as BucketChildConfig } from './BucketChildConfig.vue'; export { default as BucketConfigForm } from './BucketConfigForm.vue'; export { default as BucketList } from './BucketList.vue'; export { default as BucketPermission } from './BucketPermission.vue'; diff --git a/frontend/src/services/bucketService.ts b/frontend/src/services/bucketService.ts index 22f85a15..6ae6f723 100644 --- a/frontend/src/services/bucketService.ts +++ b/frontend/src/services/bucketService.ts @@ -25,6 +25,18 @@ export default { return comsAxios().put(`${BUCKET_PATH}`, data); }, + /** + * @function createBucketChild + * Creates a bucket + * @param {string} parentBucketId ID of parent COMS 'bucket' + * @param {string} subKey 'sub-folder' name (last part of the key) + * @param {string} bucketName Display name for the mapped sub-folder + * @returns {Promise} An axios response + */ + createBucketChild(parentBucketId: string, subKey: string, bucketName: string) { + return comsAxios().put(`${BUCKET_PATH}/${parentBucketId}/child`, { subKey, bucketName }); + }, + /** * @function deleteBucket * Deletes a bucket diff --git a/frontend/src/store/bucketStore.ts b/frontend/src/store/bucketStore.ts index d7b0d549..dae9eaa2 100644 --- a/frontend/src/store/bucketStore.ts +++ b/frontend/src/store/bucketStore.ts @@ -41,6 +41,15 @@ export const useBucketStore = defineStore('bucket', () => { } } + async function createBucketChild(parentBucketId: string, subKey: string, bucketName: string) { + try { + appStore.beginIndeterminateLoading(); + return await bucketService.createBucketChild(parentBucketId, subKey, bucketName); + } finally { + appStore.endIndeterminateLoading(); + } + } + async function deleteBucket(bucketId: string) { try { appStore.beginIndeterminateLoading(); @@ -120,6 +129,7 @@ export const useBucketStore = defineStore('bucket', () => { // Actions createBucket, + createBucketChild, deleteBucket, fetchBuckets, findBucketById, diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 0a91ecc5..77c3c531 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -127,3 +127,14 @@ export function setDispositionHeader(filename: string) { export function getBucketPath(bucket: Bucket): string { return `${bucket.endpoint}/${bucket.bucket}/${bucket.key}`; } + +/** + * @function getLastSegment + * Returns the last segment of a path, ignoring trailing slashes + * @param {string} path full path (eg: http://abc.com/bucket/) + * @returns {string} last segment of a path (eg: bucket) + */ +export function getLastSegment(path: string) { + const p = path.replace(/\/+$/, ''); + return p.slice(p.lastIndexOf('/') + 1); +}