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);
+}