web/satellite/vuetify-poc: add create bucket dialog

This change adds the ability to create a bucket to the vuetify poc.

Issue: https://github.com/storj/storj/issues/6112

Change-Id: Ib56432e2ad09c6673c9903a49537199882fd1cf3
This commit is contained in:
Wilfred Asomani 2023-08-11 08:52:20 +00:00 committed by Storj Robot
parent da08117fcd
commit d0f5f06159
4 changed files with 347 additions and 90 deletions

View File

@ -0,0 +1,340 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="auto"
min-width="400px"
max-width="450px"
transition="fade-transition"
>
<v-card ref="innerContent" rounded="xlg">
<v-sheet>
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-card-title class="font-weight-bold">
<!-- <img src="../assets/icon-bucket-color.svg" alt="Bucket" width="40"> -->
Create New Bucket
</v-card-title>
</template>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="model = false"
/>
</template>
</v-card-item>
</v-sheet>
<v-divider />
<v-form v-model="formValid" class="pa-8 pb-3">
<v-row>
<v-col>
<p>Buckets are used to store and organize your files.</p>
<v-text-field
v-model="bucketName"
variant="outlined"
:rules="bucketNameRules"
label="Enter bucket name"
hint="Lowercase letters, numbers, hyphens (-), and periods (.)"
hide-details="auto"
required
autofocus
class="mt-8 mb-3"
/>
</v-col>
</v-row>
</v-form>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn :disabled="isLoading" variant="outlined" color="default" block @click="model = false">Cancel</v-btn>
</v-col>
<v-col>
<v-btn :disabled="!formValid" :loading="isLoading" color="primary" variant="flat" block @click="onCreate">
Create Bucket
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { Component, computed, ref, watch } from 'vue';
import {
VBtn,
VCard,
VCardActions,
VCardItem,
VCardTitle,
VCol,
VDialog,
VDivider,
VForm,
VRow, VSheet,
VTextField,
} from 'vuetify/components';
import { useLoading } from '@/composables/useLoading';
import { useConfigStore } from '@/store/modules/configStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { FILE_BROWSER_AG_NAME, useBucketsStore } from '@/store/modules/bucketsStore';
import { LocalData } from '@/utils/localData';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const usersStore = useUsersStore();
const agStore = useAccessGrantsStore();
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
const analyticsStore = useAnalyticsStore();
const configStore = useConfigStore();
const innerContent = ref<Component | null>(null);
const props = defineProps<{
modelValue: boolean,
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void,
}>();
const formValid = ref<boolean>(false);
const bucketName = ref<string>('');
const worker = ref<Worker | null>(null);
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const bucketNameRules = computed(() => {
return [
(value: string) => (!!value || 'Bucket name is required.'),
(value: string) => ((value.length >= 3 && value.length <= 63) || 'Bucket name must be not less than 3 and not more than 63 characters length.'),
(value: string) => {
if (/^[a-z0-9-.]+$/.test(value)) return true;
if (/[A-Z]/.test(value)) return 'Uppercase characters are not allowed.';
if (/\s/.test(value)) return 'Spaces are not allowed.';
if (/[^a-zA-Z0-9-.]/.test(value)) return 'Other characters are not allowed.';
return true;
},
(value: string) => (!allBucketNames.value.includes(value) || 'A bucket exists with this name.'),
];
});
/**
* Returns all bucket names from store.
*/
const allBucketNames = computed((): string[] => {
return bucketsStore.state.allBucketNames;
});
/**
* Returns condition if user has to be prompt for passphrase from store.
*/
const promptForPassphrase = computed((): boolean => {
return bucketsStore.state.promptForPassphrase;
});
/**
* Returns object browser api key from store.
*/
const apiKey = computed((): string => {
return bucketsStore.state.apiKey;
});
/**
* Returns edge credentials from store.
*/
const edgeCredentials = computed((): EdgeCredentials => {
return bucketsStore.state.edgeCredentials;
});
/**
* Returns edge credentials for bucket creation from store.
*/
const edgeCredentialsForCreate = computed((): EdgeCredentials => {
return bucketsStore.state.edgeCredentialsForCreate;
});
/**
* Indicates if bucket was created.
*/
const bucketWasCreated = computed((): boolean => {
const status = LocalData.getBucketWasCreatedStatus();
if (status !== null) {
return status;
}
return false;
});
/**
* Sets local worker with worker instantiated in store.
*/
function setWorker(): void {
worker.value = agStore.state.accessGrantsWebWorker;
if (worker.value) {
worker.value.onerror = (error: ErrorEvent) => {
notify.error(error.message, AnalyticsErrorEventSource.CREATE_BUCKET_MODAL);
};
}
}
/**
* Validates provided bucket's name and creates a bucket.
*/
function onCreate(): void {
if (!formValid.value) return;
withLoading(async () => {
if (!worker.value) {
notify.error('Worker is not defined', AnalyticsErrorEventSource.CREATE_BUCKET_MODAL);
return;
}
try {
const projectID = projectsStore.state.selectedProject.id;
if (!promptForPassphrase.value) {
if (!edgeCredentials.value.accessKeyId) {
await bucketsStore.setS3Client(projectID);
}
await bucketsStore.createBucket(bucketName.value);
await bucketsStore.getBuckets(1, projectID);
bucketsStore.setFileComponentBucketName(bucketName.value);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
}
model.value = false;
return;
}
if (edgeCredentialsForCreate.value.accessKeyId) {
await bucketsStore.createBucketWithNoPassphrase(bucketName.value);
await bucketsStore.getBuckets(1, projectID);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
}
model.value = false;
return ;
}
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': true,
'isList': false,
'isDelete': false,
'notAfter': inOneHour.toISOString(),
'buckets': JSON.stringify([]),
'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.CREATE_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.CREATE_BUCKET_MODAL);
return;
}
const accessGrant = accessGrantEvent.data.value;
const creds: EdgeCredentials = await agStore.getEdgeCredentials(accessGrant);
bucketsStore.setEdgeCredentialsForCreate(creds);
await bucketsStore.createBucketWithNoPassphrase(bucketName.value);
await bucketsStore.getBuckets(1, projectID);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
}
model.value = false;
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.CREATE_BUCKET_MODAL);
}
});
}
watch(innerContent, newContent => {
if (newContent) {
setWorker();
withLoading(async () => {
try {
await bucketsStore.getAllBucketsNames(projectsStore.state.selectedProject.id);
bucketName.value = allBucketNames.value.length > 0 ? '' : 'demo-bucket';
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.CREATE_BUCKET_MODAL);
}
});
return;
}
// dialog has been closed
bucketName.value = '';
});
</script>

View File

@ -41,6 +41,7 @@
:rules="rules"
:error-messages="isError ? 'Invalid code. Please re-enter.' : ''"
:label="useRecoveryCode ? 'Recovery code' : '2FA Code'"
hide-details="auto"
required
autofocus
/>

View File

@ -65,6 +65,7 @@
:rules="rules"
:error-messages="isError ? 'Invalid code. Please re-enter.' : ''"
label="2FA Code"
hide-details="auto"
required
autofocus
/>

View File

@ -10,85 +10,20 @@
<v-col>
<v-btn
color="primary"
@click="isCreateBucketDialogOpen = true"
>
<svg width="16" height="16" class="mr-2" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1ZM10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65ZM10.7496 6.8989L10.7499 6.91218L10.7499 9.223H12.9926C13.4529 9.223 13.8302 9.58799 13.8456 10.048C13.8602 10.4887 13.5148 10.8579 13.0741 10.8726L13.0608 10.8729L10.7499 10.873L10.75 13.171C10.75 13.6266 10.3806 13.996 9.925 13.996C9.48048 13.996 9.11807 13.6444 9.10066 13.2042L9.1 13.171L9.09985 10.873H6.802C6.34637 10.873 5.977 10.5036 5.977 10.048C5.977 9.60348 6.32857 9.24107 6.76882 9.22366L6.802 9.223H9.09985L9.1 6.98036C9.1 6.5201 9.46499 6.14276 9.925 6.12745C10.3657 6.11279 10.7349 6.45818 10.7496 6.8989Z" fill="currentColor" />
</svg>
New Bucket
<v-dialog
v-model="dialog"
activator="parent"
width="auto"
min-width="400px"
transition="fade-transition"
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-card-title class="font-weight-bold">
<!-- <img src="../assets/icon-bucket-color.svg" alt="Bucket" width="40"> -->
Create New Bucket
</v-card-title>
</template>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="dialog = false"
/>
</template>
</v-card-item>
</v-sheet>
<v-divider />
<v-form v-model="valid" class="pa-8 pb-3">
<v-row>
<v-col>
<p>Buckets are used to store and organize your files.</p>
<v-text-field
v-model="bucketName"
variant="outlined"
:rules="bucketNameRules"
label="Enter bucket name"
hint="Lowercase letters, numbers, hyphens (-), and periods (.)"
required
autofocus
class="mt-8 mb-3"
/>
</v-col>
</v-row>
</v-form>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block @click="dialog = false">Cancel</v-btn>
</v-col>
<v-col>
<v-btn color="primary" variant="flat" block>
Create Bucket
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</v-btn>
</v-col>
</v-row>
<BucketsDataTable />
</v-container>
<CreateBucketDialog v-model="isCreateBucketDialogOpen" />
</template>
<script setup lang="ts">
@ -98,32 +33,12 @@ import {
VRow,
VCol,
VBtn,
VDialog,
VCard,
VSheet,
VCardItem,
VCardTitle,
VDivider,
VForm,
VTextField,
VCardActions,
} from 'vuetify/components';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import PageSubtitleComponent from '@poc/components/PageSubtitleComponent.vue';
import BucketsDataTable from '@poc/components/BucketsDataTable.vue';
import CreateBucketDialog from '@poc/components/dialogs/CreateBucketDialog.vue';
const dialog = ref<boolean>(false);
const bucketName = ref<string>('');
const bucketNameRules = [
(value: string) => (!!value || 'Bucket name is required.'),
(value: string) => {
if (/^[a-z0-9-.]+$/.test(value)) return true;
if (/[A-Z]/.test(value)) return 'Uppercase characters are not allowed.';
if (/\s/.test(value)) return 'Spaces are not allowed.';
if (/[^a-zA-Z0-9-.]/.test(value)) return 'Other characters are not allowed.';
return true;
},
];
const isCreateBucketDialogOpen = ref<boolean>(false);
</script>