web/satellite/vuetify-poc: add bucket deletion option to buckets table

This change allows buckets to be deleted from the Buckets page through
a dropdown menu in the buckets table. Before deletion, users are
prompted to confirm their decision in a dialog box.

Revoles #6114

Change-Id: Ie3a8bfbf94a29c4bc67b4dacbace6abe7e1bc2eb
This commit is contained in:
Jeremy Wharton 2023-08-15 22:27:18 -05:00 committed by Storj Robot
parent f2e607c70f
commit 957d8d6ca0
6 changed files with 279 additions and 6 deletions

View File

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5039 4.47638C18.7779 4.47638 19 4.69847 19 4.97244C19 5.24641 18.7779 5.4685 18.5039 5.4685L16.6817 5.46836L15.4073 15.876C15.2332 17.2977 14.0259 18.3661 12.5936 18.3661H7.52282C6.10137 18.3661 4.89972 17.3134 4.71279 15.9043L3.32838 5.46836L1.49606 5.4685C1.22209 5.4685 1 5.24641 1 4.97244C1 4.69847 1.22209 4.47638 1.49606 4.47638H18.5039ZM15.6822 5.4685H4.32943L5.6963 15.7738C5.81584 16.6749 6.57385 17.3519 7.47823 17.3735L7.52282 17.374H12.5936C13.5099 17.374 14.2844 16.7014 14.4166 15.7993L14.4225 15.7554L15.6822 5.4685ZM8.47662 8.91592L8.48146 8.9424L8.4849 8.96941L8.98001 13.9215L8.98375 13.9527C9.01639 14.2247 8.82233 14.4717 8.55031 14.5043C8.28736 14.5358 8.04782 14.3556 8.00266 14.0978L7.99869 14.0709L7.99388 14.03L7.4977 9.06813C7.47043 8.79552 7.66933 8.55243 7.94194 8.52517C8.19637 8.49973 8.42509 8.67129 8.47662 8.91592ZM12.3071 8.4919L12.3342 8.49334C12.5982 8.51448 12.7973 8.73863 12.7906 9.00024L12.7891 9.02742L12.7857 9.06844L12.2899 14.0266C12.2627 14.2992 12.0196 14.4981 11.747 14.4708C11.4834 14.4445 11.2888 14.2164 11.3007 13.955L11.3027 13.9278L11.7977 8.97947L11.8002 8.94821C11.8206 8.69332 12.0302 8.49893 12.2801 8.4919H12.3071ZM12.8346 1.5C13.1086 1.5 13.3307 1.72209 13.3307 1.99606C13.3307 2.27003 13.1086 2.49213 12.8346 2.49213H7.16535C6.89139 2.49213 6.66929 2.27003 6.66929 1.99606C6.66929 1.72209 6.89139 1.5 7.16535 1.5H12.8346Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -64,24 +64,52 @@
{{ item.raw.since.toLocaleString() }}
</span>
</template>
<template #item.actions="{ item }">
<v-menu location="bottom end" transition="scale-transition">
<template #activator="{ props: activatorProps }">
<v-btn
icon="mdi-dots-vertical"
color="default"
variant="text"
size="small"
density="comfortable"
v-bind="activatorProps"
/>
</template>
<v-list class="pa-0">
<v-list-item link @click="() => showDeleteBucketDialog(item.raw.name)">
<template #prepend>
<icon-trash />
</template>
<v-list-item-title class="text-body-2 ml-3">
Delete bucket
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-data-table-server>
</v-card>
<delete-bucket-dialog v-model="isDeleteBucketDialogShown" :bucket-name="bucketToDelete" />
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { VCard, VTextField, VBtn } from 'vuetify/components';
import { VCard, VTextField, VBtn, VMenu, VList, VListItem, VListItemTitle } from 'vuetify/components';
import { VDataTableServer } from 'vuetify/labs/components';
import { BucketPage, BucketCursor } from '@/types/buckets';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
import { tableSizeOptions } from '@/types/common';
import IconTrash from '@poc/components/icons/IconTrash.vue';
import DeleteBucketDialog from '@poc/components/dialogs/DeleteBucketDialog.vue';
const bucketsStore = useBucketsStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
@ -92,6 +120,8 @@ const areBucketsFetching = ref<boolean>(true);
const search = ref<string>('');
const searchTimer = ref<NodeJS.Timeout>();
const selected = ref([]);
const isDeleteBucketDialogShown = ref<boolean>(false);
const bucketToDelete = ref<string>('');
const headers = [
{
@ -105,6 +135,7 @@ const headers = [
{ title: 'Objects', key: 'objectCount', sortable: false },
{ title: 'Segments', key: 'segmentCount', sortable: false },
{ title: 'Date Created', key: 'createdAt', sortable: false },
{ key: 'actions', width: '0', sortable: false },
];
/**
@ -166,6 +197,14 @@ function openBucket(bucketName: string): void {
router.push(`/projects/${projectsStore.state.selectedProject.id}/buckets/${bucketName}`);
}
/**
* Displays the Delete Bucket dialog.
*/
function showDeleteBucketDialog(bucketName: string): void {
bucketToDelete.value = bucketName;
isDeleteBucketDialogShown.value = true;
}
onMounted(() => {
fetchBuckets();
});

View File

@ -94,7 +94,7 @@
</template>
<v-list-item v-else link @click="declineInvitation(item.raw)">
<template #prepend>
<img src="@poc/assets/icon-trash.svg" alt="Decline">
<icon-trash />
</template>
<v-list-item-title class="text-body-2 ml-3">
Decline
@ -137,6 +137,7 @@ import { useNotify } from '@/utils/hooks';
import IconSettings from '@poc/components/icons/IconSettings.vue';
import IconTeam from '@poc/components/icons/IconTeam.vue';
import IconTrash from '@poc/components/icons/IconTrash.vue';
const props = defineProps<{
items: ProjectItemModel[],

View File

@ -0,0 +1,223 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="410px"
transition="fade-transition"
:persistent="isLoading"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"
width="40"
height="40"
rounded="lg"
>
<icon-trash />
</v-sheet>
</template>
<v-card-title class="font-weight-bold">Delete Bucket</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
<v-divider />
<div class="pa-7">
The following bucket and all of its data will be deleted. This action cannot be undone.
<br><br>
<span class="font-weight-bold">{{ bucketName }}</span>
</div>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">
Cancel
</v-btn>
</v-col>
<v-col>
<v-btn color="error" variant="flat" block :loading="isLoading" @click="onDelete">
Delete
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import {
VDialog,
VCard,
VCardItem,
VSheet,
VCardTitle,
VDivider,
VCardActions,
VRow,
VCol,
VBtn,
} from 'vuetify/components';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { useBucketsStore, FILE_BROWSER_AG_NAME } from '@/store/modules/bucketsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useLoading } from '@/composables/useLoading';
import { useNotify } from '@/utils/hooks';
import IconTrash from '@poc/components/icons/IconTrash.vue';
const props = defineProps<{
modelValue: boolean,
bucketName: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean],
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const analyticsStore = useAnalyticsStore();
const configStore = useConfigStore();
const bucketsStore = useBucketsStore();
const agStore = useAccessGrantsStore();
const projectsStore = useProjectsStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const worker = ref<Worker| null>(null);
/**
* Returns API key from store.
*/
const apiKey = computed((): string => {
return bucketsStore.state.apiKey;
});
/**
* Creates unrestricted access grant and deletes bucket
* when Delete button has been clicked.
*/
async function onDelete(): Promise<void> {
await withLoading(async () => {
const projectID = projectsStore.state.selectedProject.id;
try {
if (!worker.value) throw new Error('Web worker is not initialized.');
if (!apiKey.value) {
await agStore.deleteAccessGrantByNameAndProjectID(FILE_BROWSER_AG_NAME, projectID);
const cleanAPIKey: AccessGrant = await agStore.createAccessGrant(FILE_BROWSER_AG_NAME, projectID);
bucketsStore.setApiKey(cleanAPIKey.secret);
}
const now = new Date();
const inOneHour = new Date(now.setHours(now.getHours() + 1));
worker.value.postMessage({
'type': 'SetPermission',
'isDownload': false,
'isUpload': false,
'isList': true,
'isDelete': true,
'notAfter': inOneHour.toISOString(),
'buckets': JSON.stringify([props.bucketName]),
'apiKey': apiKey.value,
});
const grantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
if (grantEvent.data.error) {
notify.error(grantEvent.data.error, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
return;
}
const salt = await projectsStore.getProjectSalt(projectsStore.state.selectedProject.id);
const satelliteNodeURL: string = configStore.state.config.satelliteNodeURL;
worker.value.postMessage({
'type': 'GenerateAccess',
'apiKey': grantEvent.data.value,
'passphrase': '',
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const accessGrantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
if (accessGrantEvent.data.error) {
notify.error(accessGrantEvent.data.error, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
return;
}
const accessGrant = accessGrantEvent.data.value;
const edgeCredentials: EdgeCredentials = await agStore.getEdgeCredentials(accessGrant);
bucketsStore.setEdgeCredentialsForDelete(edgeCredentials);
await bucketsStore.deleteBucket(props.bucketName);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_DELETED);
await fetchBuckets();
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
return;
}
notify.success('Bucket deleted.');
model.value = false;
});
}
/**
* Fetches bucket using api.
*/
async function fetchBuckets(): Promise<void> {
try {
await bucketsStore.getBuckets(1, projectsStore.state.selectedProject.id);
} catch (error) {
notify.error(`Unable to fetch buckets. ${error.message}`, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
}
}
/**
* Sets local worker with worker instantiated in store.
*/
watch(() => agStore.state.accessGrantsWebWorker, value => {
worker.value = value;
if (!value) return;
value.onerror = (error: ErrorEvent) => {
notify.error(error.message, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
};
}, { immediate: true });
</script>

View File

@ -0,0 +1,8 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5039 4.47638C18.7779 4.47638 19 4.69847 19 4.97244C19 5.24641 18.7779 5.4685 18.5039 5.4685L16.6817 5.46836L15.4073 15.876C15.2332 17.2977 14.0259 18.3661 12.5936 18.3661H7.52282C6.10137 18.3661 4.89972 17.3134 4.71279 15.9043L3.32838 5.46836L1.49606 5.4685C1.22209 5.4685 1 5.24641 1 4.97244C1 4.69847 1.22209 4.47638 1.49606 4.47638H18.5039ZM15.6822 5.4685H4.32943L5.6963 15.7738C5.81584 16.6749 6.57385 17.3519 7.47823 17.3735L7.52282 17.374H12.5936C13.5099 17.374 14.2844 16.7014 14.4166 15.7993L14.4225 15.7554L15.6822 5.4685ZM8.47662 8.91592L8.48146 8.9424L8.4849 8.96941L8.98001 13.9215L8.98375 13.9527C9.01639 14.2247 8.82233 14.4717 8.55031 14.5043C8.28736 14.5358 8.04782 14.3556 8.00266 14.0978L7.99869 14.0709L7.99388 14.03L7.4977 9.06813C7.47043 8.79552 7.66933 8.55243 7.94194 8.52517C8.19637 8.49973 8.42509 8.67129 8.47662 8.91592ZM12.3071 8.4919L12.3342 8.49334C12.5982 8.51448 12.7973 8.73863 12.7906 9.00024L12.7891 9.02742L12.7857 9.06844L12.2899 14.0266C12.2627 14.2992 12.0196 14.4981 11.747 14.4708C11.4834 14.4445 11.2888 14.2164 11.3007 13.955L11.3027 13.9278L11.7977 8.97947L11.8002 8.94821C11.8206 8.69332 12.0302 8.49893 12.2801 8.4919H12.3071ZM12.8346 1.5C13.1086 1.5 13.3307 1.72209 13.3307 1.99606C13.3307 2.27003 13.1086 2.49213 12.8346 2.49213H7.16535C6.89139 2.49213 6.66929 2.27003 6.66929 1.99606C6.66929 1.72209 6.89139 1.5 7.16535 1.5H12.8346Z" fill="currentColor" />
</svg>
</template>

View File

@ -313,3 +313,8 @@ table {
.v-alert__close .v-btn {
color: rgb(var(--v-theme-on-surface)) !important;
}
.bg-on-surface-variant {
background-color: rgb(var(--v-theme-on-surface-variant)) !important;
color: rgb(var(--v-theme-surface-variant)) !important;
}