Skip to content

Commit

Permalink
web/satellite: improved project level passphrase experience
Browse files Browse the repository at this point in the history
Clicking continue in web toggles create project level passphrase which then redirects to project dashboard.
Added new create bucket modal.
Updated open bucket modal.
Updated project dashboard and buckets view to work correctly with no buckets state and no passphrase state.

Issue:
#5455

Change-Id: If6ddac7d3365854a02b2bb8898e4742e9d2c31c1
  • Loading branch information
VitaliiShpital authored and Storj Robot committed Jan 17, 2023
1 parent df9bbce commit 079728f
Show file tree
Hide file tree
Showing 14 changed files with 644 additions and 59 deletions.
1 change: 1 addition & 0 deletions web/satellite/src/components/browser/FileBrowser.vue
Expand Up @@ -711,6 +711,7 @@ async function goToBuckets(): Promise<void> {
font-size: 2rem;
font-weight: 500;
line-height: 1.2;
word-break: break-all;
@media screen and (max-width: 768px) {
margin-bottom: 0.5rem;
Expand Down
10 changes: 10 additions & 0 deletions web/satellite/src/components/modals/AllModals.vue
Expand Up @@ -23,6 +23,7 @@
<NewBillingAddCouponCodeModal v-if="isNewBillingAddCouponModal" />
<CreateProjectPassphraseModal v-if="isCreateProjectPassphraseModal" />
<ManageProjectPassphraseModal v-if="isManageProjectPassphraseModal" />
<CreateBucketModal v-if="isCreateBucketModal" />
</div>
</template>

Expand All @@ -31,6 +32,7 @@ import { Component, Vue } from 'vue-property-decorator';
import CreateProjectPromptModal from '@/components/modals/CreateProjectPromptModal.vue';
import CreateProjectModal from '@/components/modals/CreateProjectModal.vue';
import CreateBucketModal from '@/components/modals/CreateBucketModal.vue';
import AddPaymentMethodModal from '@/components/modals/AddPaymentMethodModal.vue';
import OpenBucketModal from '@/components/modals/OpenBucketModal.vue';
import MFARecoveryCodesModal from '@/components/modals/MFARecoveryCodesModal.vue';
Expand Down Expand Up @@ -58,6 +60,7 @@ import ManageProjectPassphraseModal from '@/components/modals/manageProjectPassp
DeleteBucketModal,
CreateProjectPromptModal,
CreateProjectModal,
CreateBucketModal,
AddPaymentMethodModal,
OpenBucketModal,
MFARecoveryCodesModal,
Expand Down Expand Up @@ -216,5 +219,12 @@ export default class AllModals extends Vue {
public get isManageProjectPassphraseModal(): boolean {
return this.$store.state.appStateModule.appState.isManageProjectPassphraseModalShown;
}
/**
* Indicates if create bucket modal is shown.
*/
public get isCreateBucketModal(): boolean {
return this.$store.state.appStateModule.appState.isCreateBucketModalShown;
}
}
</script>
256 changes: 256 additions & 0 deletions web/satellite/src/components/modals/CreateBucketModal.vue
@@ -0,0 +1,256 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<CreateBucketIcon class="modal__icon" />
<h1 class="modal__title" aria-roledescription="modal-title">
Create a Bucket
</h1>
<p class="modal__info">
Buckets are used to store and organize your files. Enter lowercase alphanumeric characters only,
no spaces.
</p>
<VLoader v-if="bucketNamesLoading" width="100px" height="100px" />
<VInput
v-else
:init-value="bucketName"
label="Bucket Name"
placeholder="Enter bucket name"
class="full-input"
:error="nameError"
@setData="setBucketName"
/>
<div class="modal__button-container">
<VButton
label="Cancel"
width="100%"
height="48px"
font-size="14px"
:on-press="closeModal"
:is-transparent="true"
/>
<VButton
label="Create bucket"
width="100%"
height="48px"
font-size="14px"
:on-press="onCreate"
:is-disabled="!bucketName"
/>
</div>
<div v-if="isLoading" class="modal__blur">
<VLoader class="modal__blur__loader" width="50px" height="50px" />
</div>
</div>
</template>
</VModal>
</template>

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { RouteConfig } from '@/router';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify, useRouter, useStore } from '@/utils/hooks';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { Validator } from '@/utils/validation';
import VLoader from '@/components/common/VLoader.vue';
import VInput from '@/components/common/VInput.vue';
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import CreateBucketIcon from '@/../static/images/buckets/createBucket.svg';
const store = useStore();
const notify = useNotify();
const router = useRouter();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const bucketName = ref<string>('');
const nameError = ref<string>('');
const bucketNamesLoading = ref<boolean>(true);
const isLoading = ref<boolean>(false);
/**
* Returns all bucket names from store.
*/
const allBucketNames = computed((): string[] => {
return store.state.bucketUsageModule.allBucketNames;
});
/**
* Validates provided bucket's name and creates a bucket.
*/
async function onCreate(): Promise<void> {
if (isLoading.value) return;
if (!isBucketNameValid(bucketName.value)) {
analytics.errorEventTriggered(AnalyticsErrorEventSource.BUCKET_CREATION_NAME_STEP);
return;
}
if (allBucketNames.value.includes(bucketName.value)) {
notify.error('Bucket with this name already exists', AnalyticsErrorEventSource.BUCKET_CREATION_NAME_STEP);
return;
}
isLoading.value = true;
try {
await store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET, bucketName.value);
await store.dispatch(BUCKET_ACTIONS.FETCH, 1);
await store.dispatch(OBJECTS_ACTIONS.SET_FILE_COMPONENT_BUCKET_NAME, bucketName.value);
analytics.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
analytics.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
await router.push(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
closeModal();
} catch (error) {
await notify.error(`Unable to fetch buckets. ${error.message}`, AnalyticsErrorEventSource.BUCKET_CREATION_FLOW);
}
isLoading.value = false;
}
/**
* Sets bucket name value from input to local variable.
*/
function setBucketName(name: string): void {
bucketName.value = name;
}
/**
* Closes create bucket modal.
*/
function closeModal(): void {
store.commit(APP_STATE_MUTATIONS.TOGGLE_CREATE_BUCKET_MODAL_SHOWN);
}
/**
* Returns validation status of a bucket name.
*/
function isBucketNameValid(name: string): boolean {
switch (true) {
case name.length < 3 || name.length > 63:
nameError.value = 'Name must be not less than 3 and not more than 63 characters length';
return false;
case !Validator.bucketName(name):
nameError.value = 'Name must contain only lowercase latin characters, numbers, a hyphen or a period';
return false;
default:
return true;
}
}
onMounted(async (): Promise<void> => {
try {
await store.dispatch(BUCKET_ACTIONS.FETCH_ALL_BUCKET_NAMES);
bucketName.value = allBucketNames.value.length > 0 ? '' : 'demo-bucket';
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.BUCKET_CREATION_NAME_STEP);
} finally {
bucketNamesLoading.value = false;
}
});
</script>

<style scoped lang="scss">
.modal {
width: 430px;
padding: 43px 60px 66px;
display: flex;
align-items: center;
flex-direction: column;
font-family: 'font_regular', sans-serif;
@media screen and (max-width: 600px) {
width: calc(100% - 48px);
padding: 54px 24px 32px;
}
&__icon {
max-height: 154px;
max-width: 118px;
}
&__title {
font-family: 'font_bold', sans-serif;
font-size: 28px;
line-height: 34px;
color: #1b2533;
margin-top: 20px;
text-align: center;
@media screen and (max-width: 600px) {
margin-top: 16px;
font-size: 24px;
line-height: 31px;
}
}
&__info {
font-family: 'font_regular', sans-serif;
font-size: 16px;
line-height: 21px;
text-align: center;
color: #354049;
margin: 20px 0 0;
}
&__button-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30px;
column-gap: 20px;
@media screen and (max-width: 600px) {
margin-top: 20px;
column-gap: unset;
row-gap: 8px;
flex-direction: column-reverse;
}
}
&__blur {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgb(229 229 229 / 20%);
border-radius: 8px;
z-index: 100;
&__loader {
width: 25px;
height: 25px;
position: absolute;
right: 40px;
top: 40px;
}
}
}
.full-input {
margin-top: 20px;
}
:deep(.label-container) {
margin-bottom: 8px;
}
:deep(.label-container__main__label) {
font-family: 'font_bold', sans-serif;
font-size: 14px;
color: #56606d;
}
</style>
38 changes: 25 additions & 13 deletions web/satellite/src/components/modals/OpenBucketModal.vue
Expand Up @@ -5,18 +5,28 @@
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<Icon />
<h1 class="modal__title">Open a Bucket</h1>
<p class="modal__info">
To open a bucket and view your files, please enter the encryption passphrase you saved upon creating this bucket.
</p>
<VInput
class="modal__input"
label="Bucket Name"
:init-value="bucketName"
role-description="bucket"
:disabled="true"
/>
<template v-if="isNewEncryptionPassphraseFlowEnabled">
<OpenBucketIcon />
<h1 class="modal__title">Enter your encryption passphrase</h1>
<p class="modal__info">
To open a bucket and view your encrypted files, <br>please enter your encryption passphrase.
</p>
</template>
<template v-else>
<Icon />
<h1 class="modal__title">Open a Bucket</h1>
<p class="modal__info">
To open a bucket and view your files, please enter the encryption passphrase you saved upon
creating this bucket.
</p>
<VInput
class="modal__input"
label="Bucket Name"
:init-value="bucketName"
role-description="bucket"
:disabled="true"
/>
</template>
<VInput
label="Encryption Passphrase"
placeholder="Enter a passphrase here"
Expand Down Expand Up @@ -64,14 +74,16 @@ import VInput from '@/components/common/VInput.vue';
import VButton from '@/components/common/VButton.vue';
import Icon from '@/../static/images/objects/openBucket.svg';
import OpenBucketIcon from '@/../static/images/buckets/openBucket.svg';
// @vue/component
@Component({
components: {
VInput,
VModal,
Icon,
VButton,
Icon,
OpenBucketIcon,
},
})
export default class OpenBucketModal extends Vue {
Expand Down
Expand Up @@ -60,14 +60,15 @@
import { computed, onMounted, ref } from 'vue';
import { generateMnemonic } from 'bip39';
import { useNotify, useStore } from '@/utils/hooks';
import { useNotify, useRoute, useRouter, useStore } from '@/utils/hooks';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { OBJECTS_ACTIONS, OBJECTS_MUTATIONS } from '@/store/modules/objects';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { MetaUtils } from '@/utils/meta';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { RouteConfig } from '@/router';
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
Expand All @@ -94,6 +95,8 @@ const FILE_BROWSER_AG_NAME = 'Web file browser API key';
const store = useStore();
const notify = useNotify();
const route = useRoute();
const router = useRouter();
const selectedOption = ref<CreatePassphraseOption>(CreatePassphraseOption.Generate);
const activeStep = ref<CreateProjectPassphraseStep>(CreateProjectPassphraseStep.SelectMode);
Expand Down Expand Up @@ -299,6 +302,10 @@ async function onContinue(): Promise<void> {
}
if (activeStep.value === CreateProjectPassphraseStep.Success) {
if (route?.name === RouteConfig.OverviewStep.name) {
router.push(RouteConfig.ProjectDashboard.path);
}
closeModal();
}
}
Expand Down

0 comments on commit 079728f

Please sign in to comment.