Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module.exports = {
'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }],
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
quotes: ['error', 'single'],
quotes: ['error', 'single', { avoidEscape: true }],
semi: ['error', 'always']
},
overrides: [
Expand Down
2 changes: 1 addition & 1 deletion frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = {
'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }],
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
quotes: ['error', 'single'],
quotes: ['error', 'single', { avoidEscape: true }],
semi: ['error', 'always'],
'vue/component-tags-order': [
'error',
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/assets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,21 @@ div:focus-visible {
font-weight: 400;
}

.p-tag-danger {
background-color: #fef1d8;
outline-style: solid;
outline-color: #f8bb47;
outline-width: thin;

.p-tag-value {
color: #474543;
}

.p-tag-icon {
color: #474543
}
}

/* datatable */
.p-datatable,
.p-treetable {
Expand Down Expand Up @@ -312,6 +327,7 @@ div:focus-visible {
}
.action-buttons {
text-align: right;
white-space: nowrap;
// width: 150px;
}

Expand Down
14 changes: 7 additions & 7 deletions frontend/src/components/bucket/BucketConfigForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,33 +132,33 @@ const onCancel = () => {
<TextInput
name="bucketName"
label="Folder name *"
placeholder="My Documents"
placeholder="eg: My Documents"
help-text="Your custom display name for the storage location,
shown in BCBox as a folder. Any name as you would like to see it listed in BCBox."
shown in BCBox as a folder."
focus-trap
/>
<TextInput
name="bucket"
label="Bucket *"
placeholder="bucket0123456789"
placeholder="eg: mybucket"
:help-text="'The name of the bucket given to you. For example: \'yxwgj\'.'"
/>
<TextInput
name="endpoint"
label="Endpoint *"
placeholder="https://example.com"
placeholder="eg: https://nrs.objectstore.gov.bc.ca"
help-text="The URL of your object storage namespace without the bucket identifier/name."
/>
<Password
name="accessKeyId"
label="Access key identifier / User account *"
placeholder="username"
label="Access key ID *"
placeholder=""
help-text="User/Account identifier or username."
/>
<Password
name="secretAccessKey"
label="Secret access key *"
placeholder="password"
placeholder=""
help-text="A password used to access the bucket."
/>
<TextInput
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/components/bucket/BucketPermission.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { storeToRefs } from 'pinia';
import { computed, onBeforeMount, ref } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

import BucketPublicToggle from '@/components/bucket/BucketPublicToggle.vue';
import BucketPermissionAddUser from '@/components/bucket/BucketPermissionAddUser.vue';
import { BulkPermission } from '@/components/common';
import { useAlert } from '@/composables/useAlert';
import { Button, Checkbox, Column, DataTable, Message, TabPanel, TabView } from '@/lib/primevue';

import { useBucketStore, usePermissionStore } from '@/store';
import { useAuthStore, useBucketStore, usePermissionStore } from '@/store';
import { Permissions } from '@/utils/constants';

import type { Ref } from 'vue';
Expand All @@ -25,6 +26,8 @@ const props = withDefaults(defineProps<Props>(), {});
const permissionStore = usePermissionStore();
const bucketStore = useBucketStore();
const { getMappedBucketToUserPermissions } = storeToRefs(permissionStore);
const { getUserId } = storeToRefs(useAuthStore());


// State
const showSearchUsers: Ref<boolean> = ref(false);
Expand Down Expand Up @@ -78,6 +81,25 @@ onBeforeMount(async () => {
<template>
<TabView>
<TabPanel header="Manage permissions">
<!-- public toggle -->
<div class="flex flex-row gap-6 pb-3">
<div>
<h3 class="pb-1">Public</h3>
<ul>
<li>This option toggles all files in this folder to be publicly available and accessible to anyone</li>
<li>To instead set explicit permissions, add users and use the options below</li>
</ul>
</div>
<BucketPublicToggle
v-if="bucket && getUserId"
class="ml-4"
:bucket-id="bucket.bucketId"
:bucket-name="bucket.bucketName"
:bucket-public="bucket.public"
:user-id="getUserId"
/>
</div>
<!-- user search -->
<div v-if="!showSearchUsers">
<Button
class="mt-1 mb-4"
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/components/bucket/BucketPublicToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref, watch, computed, onMounted } from 'vue';

import { InputSwitch, useConfirm, useToast } from '@/lib/primevue';
import { useBucketStore, usePermissionStore } from '@/store';
import { Permissions } from '@/utils/constants';

import type { Ref } from 'vue';
import { getBucketPath, trimUrl } from '@/utils/utils';

// Props
type Props = {
bucketId: string;
bucketName: string;
bucketPublic: boolean;
userId: string;
};

const props = withDefaults(defineProps<Props>(), {});

// Store
const bucketStore = useBucketStore();
const permissionStore = usePermissionStore();

// State
const isPublic: Ref<boolean> = ref(props.bucketPublic);
const isParentPublic = ref<boolean>(false);

// Actions
const toast = useToast();
const confirm = useConfirm();

const togglePublic = async (setPublicValue: boolean) => {
if (setPublicValue) {
confirm.require({
message:
'Please confirm that you want to set folder to public. ' +
'This allows the share link to be accessible by anyone, even without credentials.',
header: 'Confirm set to public',
acceptLabel: 'Confirm',
rejectLabel: 'Cancel',
accept: () => {
bucketStore
.togglePublic(props.bucketId, true)
.then(() => {
toast.success(`"${props.bucketName}" set to public`);
})
.catch((e) => toast.error('Changing public state', e.response?.data.detail ?? e, { life: 0 }));
},
reject: () => (isPublic.value = false),
onHide: () => (isPublic.value = false)
});
} else
bucketStore
.togglePublic(props.bucketId, false)
.then(() => {
toast.success(`"${props.bucketName}" is no longer public`);
})
.catch((e) => toast.error('Changing public state', e.response?.data.detail ?? e, { life: 0 }));
};

const isParentBucketPublic = (): boolean => {
// @ts-ignore: bucketId will always be provided
const currBucketPath = getBucketPath(bucketStore.getBucket(props.bucketId));
const parentPath = currBucketPath.substring(0, currBucketPath.lastIndexOf('/'));
const parentBucket = bucketStore.getBucketByFullPath(parentPath);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think using the store is ok for now..
but i think it requires that the user has visited the 'my files' page already to load any parent folders into the store.


const isTopLevelBucket = trimUrl(currBucketPath) === trimUrl(parentPath);

// top-level buckets by definition won't have a parent bucket restricting public status
return isTopLevelBucket ? false : parentBucket?.public ?? false;
};

const isToggleEnabled = computed(() => {
return (
!isParentPublic.value &&
usePermissionStore().isUserElevatedRights() &&
permissionStore.isObjectActionAllowed(props.bucketId, props.userId, Permissions.MANAGE, props.bucketId as string)
);
});

onMounted(async () => {
isPublic.value = props.bucketPublic;
isParentPublic.value = await isParentBucketPublic();
});

watch(props, () => {
isPublic.value = props.bucketPublic;
});
</script>

<template>
<InputSwitch
v-model="isPublic"
aria-label="Toggle to make public"
:disabled="!isToggleEnabled"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe not a requirement now.. but at some point would be good to add a tooltip indicating why it's disabled.

@change="togglePublic(isPublic)"
/>
</template>
58 changes: 48 additions & 10 deletions frontend/src/components/bucket/BucketTable.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { computed, nextTick, ref, watch } from 'vue';

import { BucketChildConfig, BucketPermission, BucketTableBucketName } from '@/components/bucket';
import { Spinner } from '@/components/layout';
import { SyncButton, ShareButton } from '@/components/common';
import { Button, Column, Dialog, TreeTable, useConfirm } from '@/lib/primevue';
import { Button, Column, Dialog, Tag, TreeTable, useConfirm } from '@/lib/primevue';
import { useAppStore, useAuthStore, useBucketStore, useNavStore, usePermissionStore } from '@/store';
import { DELIMITER, Permissions } from '@/utils/constants';
import { getBucketPath, joinPath, onDialogHide } from '@/utils/utils';
Expand Down Expand Up @@ -40,6 +40,24 @@ const columnProps = {
tabindex: 0
})
};
const getBucketPublicStatus = computed(() => {
return (node: BucketTreeNode): boolean => {
if (node.data.dummy) {
return false;
} else if (node.data.bucketId) {
const storeBucket = getBuckets.value.find((bucket) => bucket.bucketId === node.data.bucketId);
return storeBucket?.public || false;
}

const nodePath = getBucketPath(node.data);
const matchingBucket = getBuckets.value.find((bucket) => {
const bucketPath = getBucketPath(bucket);
return nodePath.startsWith(bucketPath) || bucketPath.startsWith(nodePath);
});

return matchingBucket?.public || false;
};
});

const emit = defineEmits(['show-bucket-config', 'show-sidebar-info']);

Expand Down Expand Up @@ -81,7 +99,7 @@ const confirmDeleteBucket = (bucketId: string) => {
});
};

async function deleteBucket(bucketId: string, recursive=true) {
async function deleteBucket(bucketId: string, recursive = true) {
await bucketStore.deleteBucket(bucketId, recursive);
await bucketStore.fetchBuckets({ userId: getUserId.value, objectPerms: true });
}
Expand Down Expand Up @@ -147,6 +165,7 @@ function createDummyNodes(neighbour: BucketTreeNode, node: BucketTreeNode) {
dummy: true,
endpoint: node.data.endpoint,
key: key,
public: false,
region: '',
secretAccessKey: ''
},
Expand All @@ -166,10 +185,12 @@ function createDummyNodes(neighbour: BucketTreeNode, node: BucketTreeNode) {
}

watch(getBuckets, () => {
// Make sure everything is clear for a rebuild
// Make sure everything is clear for a rebuild...
endpointMap.clear();
bucketTreeNodeMap.clear();
expandedKeys.value = {};

// ...except for what rows are curerently expanded
const currentExpandedKeys = { ...expandedKeys.value };

// Split buckets into arrays based on endpoint
for (const bucket of getBuckets.value) {
Expand Down Expand Up @@ -223,6 +244,7 @@ watch(getBuckets, () => {
dummy: true,
endpoint: node.data.endpoint,
key: '/',
public: false,
region: '',
secretAccessKey: ''
},
Expand All @@ -245,6 +267,11 @@ watch(getBuckets, () => {

//set tree state
treeData.value = tree;

// restore previously-expanded TreeTable rows
nextTick(() => {
expandedKeys.value = currentExpandedKeys;
});
});
</script>

Expand Down Expand Up @@ -299,9 +326,20 @@ watch(getBuckets, () => {
header-class="text-right"
body-class="action-buttons"
header-style="width: 320px"
>
>
<template #body="{ node }">
<span v-if="!node.data.dummy">
<Tag
v-if="getBucketPublicStatus(node)"
v-tooltip="
'This folder and its contents are set to public. Change the settings in &quot;Folder permissions.&quot;'
"
value="Public"
severity="danger"
rounded
icon="pi pi-info-circle"
class="public-folder"
/>
<ShareButton
:bucket-id="node.data.bucketId"
label-text="Folder"
Expand Down Expand Up @@ -342,17 +380,17 @@ watch(getBuckets, () => {
aria-label="Folder details"
@click="showSidebarInfo(node.data.bucketId)"
>
<span class="material-icons-outlined">info</span>
</Button>
<span class="material-icons-outlined">info</span>
</Button>
<Button
v-if="permissionStore.isBucketActionAllowed(node.data.bucketId, getUserId, Permissions.DELETE)"
v-tooltip.bottom="'Delete folder'"
class="p-button-lg p-button-text p-button-danger"
aria-label="Delete folder"
@click="confirmDeleteBucket(node.data.bucketId)"
>
<span class="material-icons-outlined">delete</span>
</Button>
<span class="material-icons-outlined">delete</span>
</Button>
</span>
</template>
</Column>
Expand Down
Loading
Loading