web/satellite/vuetify-poc: implement browser onboarding flow

This change implements the browser onboarding flow in the Vuetify
project.

Resolves #6334

Change-Id: I68ff214730cb0a39ea5f9af47d9ecbe3051f2005
This commit is contained in:
Jeremy Wharton 2023-10-04 13:33:08 -05:00 committed by Storj Robot
parent cfbb5dac14
commit 22ad017f12
7 changed files with 147 additions and 29 deletions

View File

@ -9,6 +9,7 @@ export class LocalData {
private static bucketWasCreated = 'bucketWasCreated';
private static demoBucketCreated = 'demoBucketCreated';
private static bucketGuideHidden = 'bucketGuideHidden';
private static fileGuideHidden = 'fileGuideHidden';
private static serverSideEncryptionBannerHidden = 'serverSideEncryptionBannerHidden';
private static serverSideEncryptionModalHidden = 'serverSideEncryptionModalHidden';
private static sessionExpirationDate = 'sessionExpirationDate';
@ -57,6 +58,20 @@ export class LocalData {
return value === 'true';
}
/**
* Hides the "click on the file name to preview" tooltip that appears on first upload.
*/
public static setFileGuideHidden(): void {
localStorage.setItem(LocalData.fileGuideHidden, 'true');
}
/**
* Returns whether the "click on the file name to preview" tooltip that appears on first upload should be shown.
*/
public static getFileGuideHidden(): boolean {
return localStorage.getItem(LocalData.fileGuideHidden) === 'true';
}
/**
* "Disable" showing the server-side encryption banner on the bucket page
*/

View File

@ -47,7 +47,22 @@
@click="onFileClick(item.raw.browserObject)"
>
<img :src="item.raw.typeInfo.icon" :alt="item.raw.typeInfo.title + 'icon'" class="mr-3">
{{ item.raw.browserObject.Key }}
<v-tooltip
v-if="firstFile && item.raw.browserObject.Key === firstFile.Key"
:model-value="isFileGuideShown"
persistent
no-click-animation
location="bottom"
class="browser-table__file-guide"
content-class="py-2"
@update:model-value="() => {}"
>
Click on the file name to preview.
<template #activator="{ props: activatorProps }">
<span v-bind="activatorProps">{{ item.raw.browserObject.Key }}</span>
</template>
</v-tooltip>
<template v-else>{{ item.raw.browserObject.Key }}</template>
</v-btn>
</template>
@ -94,11 +109,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
VCard,
VTextField,
VBtn,
} from 'vuetify/components';
import { VCard, VTextField, VBtn, VTooltip } from 'vuetify/components';
import { VDataTableServer, VDataTableRow } from 'vuetify/labs/components';
import {
@ -115,6 +126,7 @@ import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { tableSizeOptions } from '@/types/common';
import { LocalData } from '@/utils/localData';
import BrowserRowActions from '@poc/components/BrowserRowActions.vue';
import FilePreviewDialog from '@poc/components/dialogs/FilePreviewDialog.vue';
@ -181,6 +193,7 @@ const fileToDelete = ref<BrowserObject | null>(null);
const isDeleteFileDialogShown = ref<boolean>(false);
const fileToShare = ref<BrowserObject | null>(null);
const isShareDialogShown = ref<boolean>(false);
const isFileGuideShown = ref<boolean>(false);
const sortBy = [{ key: 'name', order: 'asc' }];
const headers = [
@ -303,6 +316,13 @@ const tableFiles = computed<BrowserObjectWrapper[]>(() => {
return files.slice((opts.page - 1) * opts.itemsPerPage, opts.page * opts.itemsPerPage);
});
/**
* Returns the first browser object in the table that is a file.
*/
const firstFile = computed<BrowserObject | null>(() => {
return tableFiles.value.find(f => f.browserObject.type === 'file')?.browserObject || null;
});
/**
* Handles page change event.
*/
@ -385,6 +405,8 @@ function onFileClick(file: BrowserObject): void {
obStore.setObjectPathForModal(obStore.state.path + file.Key);
previewDialog.value = true;
isFileGuideShown.value = false;
LocalData.setFileGuideHidden();
}
/**
@ -430,11 +452,27 @@ function onShareClick(file: BrowserObject): void {
watch(filePath, fetchFiles, { immediate: true });
watch(() => props.forceEmpty, v => !v && fetchFiles());
if (!LocalData.getFileGuideHidden()) {
const unwatch = watch(firstFile, () => {
isFileGuideShown.value = true;
LocalData.setFileGuideHidden();
unwatch();
});
}
</script>
<style scoped lang="scss">
.browser-table__loader-overlay :deep(.v-overlay__scrim) {
.browser-table {
&__loader-overlay :deep(.v-overlay__scrim) {
opacity: 1;
bottom: 0.8px;
}
&__file-guide :deep(.v-overlay__content) {
color: var(--c-white) !important;
background-color: rgb(var(--v-theme-primary)) !important;
}
}
</style>

View File

@ -75,6 +75,7 @@
<script setup lang="ts">
import { Component, computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
VBtn,
VCard,
@ -103,7 +104,7 @@ import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const usersStore = useUsersStore();
const router = useRouter();
const agStore = useAccessGrantsStore();
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
@ -218,6 +219,7 @@ function onCreate(): void {
try {
const projectID = projectsStore.state.selectedProject.id;
const bucketURL = `/projects/${projectsStore.state.selectedProject.urlId}/buckets/${bucketName.value}`;
if (!promptForPassphrase.value) {
if (!edgeCredentials.value.accessKeyId) {
@ -228,6 +230,7 @@ function onCreate(): void {
bucketsStore.setFileComponentBucketName(bucketName.value);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
await router.push(bucketURL);
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
@ -242,6 +245,7 @@ function onCreate(): void {
await bucketsStore.createBucketWithNoPassphrase(bucketName.value);
await bucketsStore.getBuckets(1, projectID);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
await router.push(bucketURL);
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
@ -282,7 +286,7 @@ function onCreate(): void {
return;
}
const salt = await projectsStore.getProjectSalt(projectsStore.state.selectedProject.id);
const salt = await projectsStore.getProjectSalt(projectID);
const satelliteNodeURL: string = configStore.state.config.satelliteNodeURL;
worker.value.postMessage({
@ -310,6 +314,7 @@ function onCreate(): void {
await bucketsStore.createBucketWithNoPassphrase(bucketName.value);
await bucketsStore.getBuckets(1, projectID);
analyticsStore.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
await router.push(bucketURL);
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();

View File

@ -92,9 +92,8 @@
<v-card-actions class="pa-7">
<v-row>
<v-col v-if="stepInfo[step].prev">
<v-col v-if="stepInfo[step].prev.value">
<v-btn
v-if="stepInfo[step].prev"
variant="outlined"
color="default"
prepend-icon="mdi-chevron-left"
@ -109,7 +108,7 @@
Cancel
</v-btn>
</v-col>
<v-col v-if="stepInfo[step].next || stepInfo[step].showNextButton">
<v-col v-if="stepInfo[step].next.value || stepInfo[step].showNextButton">
<v-btn
color="primary"
variant="flat"
@ -156,20 +155,35 @@ import ClearStep from '@poc/components/dialogs/managePassphraseSteps/ClearStep.v
import LockIcon from '@/../static/images/accessGrants/newCreateFlow/accessEncryption.svg';
type ManagePassphraseLocation = ManageProjectPassphraseStep | null | (() => (ManageProjectPassphraseStep | null));
class StepInfo {
public ref: Ref<DialogStepComponent | null> = ref<DialogStepComponent | null>(null);
public prev: Ref<ManageProjectPassphraseStep | null>;
public next: Ref<ManageProjectPassphraseStep | null>;
constructor(
public prev: ManageProjectPassphraseStep | null = null,
public next: ManageProjectPassphraseStep | (() => ManageProjectPassphraseStep) | null = null,
prev: ManagePassphraseLocation = null,
next: ManagePassphraseLocation = null,
public showCancelButton: boolean = true,
public showNextButton: boolean = true,
) {}
) {
this.prev = (typeof prev === 'function')
? computed<ManageProjectPassphraseStep | null>(prev)
: ref<ManageProjectPassphraseStep | null>(prev);
this.next = (typeof next === 'function')
? computed<ManageProjectPassphraseStep | null>(next)
: ref<ManageProjectPassphraseStep | null>(next);
}
}
const props = defineProps<{
modelValue: boolean,
}>();
const props = withDefaults(defineProps<{
modelValue: boolean;
isCreate: boolean;
}>(), {
modelValue: false,
isCreate: false,
});
const model = computed<boolean>({
get: () => props.modelValue,
@ -183,7 +197,11 @@ const emit = defineEmits<{
const projectsStore = useProjectsStore();
const innerContent = ref<Component | null>(null);
const step = ref<ManageProjectPassphraseStep>(ManageProjectPassphraseStep.ManageOptions);
const step = ref<ManageProjectPassphraseStep>(
props.isCreate
? ManageProjectPassphraseStep.EncryptionPassphrase
: ManageProjectPassphraseStep.ManageOptions,
);
const passphraseOption = ref<PassphraseOption>(PassphraseOption.EnterPassphrase);
const passphrase = ref<string>('');
@ -197,7 +215,7 @@ const stepInfo: Record<ManageProjectPassphraseStep, StepInfo> = {
ManageProjectPassphraseStep.EncryptionPassphrase,
),
[ManageProjectPassphraseStep.EncryptionPassphrase]: new StepInfo(
ManageProjectPassphraseStep.Create,
() => props.isCreate ? null : ManageProjectPassphraseStep.Create,
() => passphraseOption.value === PassphraseOption.GeneratePassphrase
? ManageProjectPassphraseStep.PassphraseGenerated
: ManageProjectPassphraseStep.EnterPassphrase,
@ -221,10 +239,12 @@ function onBackClick(): void {
info.ref.value?.onExit?.('prev');
if (info.prev !== null) {
step.value = info.prev;
const prev = info.prev.value;
if (prev !== null) {
step.value = prev;
return;
}
model.value = false;
}
@ -235,7 +255,7 @@ function onNextClick(): void {
info.ref.value?.onExit?.('next');
const next = typeof info.next === 'function' ? info.next() : info.next;
const next = info.next.value;
if (next !== null) {
step.value = next;
return;
@ -271,6 +291,8 @@ watch(step, newStep => {
watch(innerContent, comp => {
if (comp) return;
step.value = ManageProjectPassphraseStep.ManageOptions;
step.value = props.isCreate
? ManageProjectPassphraseStep.EncryptionPassphrase
: ManageProjectPassphraseStep.ManageOptions;
});
</script>

View File

@ -24,7 +24,7 @@
<script setup lang="ts">
import { Component } from 'vue';
import { VSheet, VListItem, VSpacer, VIcon } from 'vuetify/components';
import { VSheet, VListItem, VIcon, VListItemTitle, VListItemSubtitle } from 'vuetify/components';
import { ManageProjectPassphraseStep } from '@poc/types/managePassphrase';
import { DialogStepComponent } from '@poc/types/common';

View File

@ -3,6 +3,27 @@
<template>
<v-container>
<v-row v-if="promptForPassphrase && !bucketWasCreated" class="mt-10 mb-15">
<v-col cols="12">
<p class="text-h5 font-weight-bold">Set an encryption passphrase<br>to start uploading files.</p>
</v-col>
<v-col cols="12">
<v-btn append-icon="mdi-chevron-right" @click="isSetPassphraseDialogShown = true">
Set Encryption Passphrase
</v-btn>
</v-col>
</v-row>
<v-row v-else-if="!promptForPassphrase && !bucketWasCreated && !bucketsCount" class="mt-10 mb-15">
<v-col cols="12">
<p class="text-h5 font-weight-bold">Create a bucket to start<br>uploading data in your project.</p>
</v-col>
<v-col cols="12">
<v-btn append-icon="mdi-chevron-right" @click="isCreateBucketDialogShown = true">
Create a Bucket
</v-btn>
</v-col>
</v-row>
<v-row align="center" justify="space-between">
<v-col cols="12" md="auto">
<PageTitleComponent title="Project Overview" />
@ -142,6 +163,8 @@
</v-container>
<edit-project-limit-dialog v-model="isEditLimitDialogShown" :limit-type="limitToChange" />
<create-bucket-dialog v-model="isCreateBucketDialogShown" />
<manage-passphrase-dialog v-model="isSetPassphraseDialogShown" is-create />
</template>
<script setup lang="ts">
@ -162,6 +185,7 @@ import { ChartUtils } from '@/utils/chart';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@poc/store/appStore';
import { LocalData } from '@/utils/localData';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import PageSubtitleComponent from '@poc/components/PageSubtitleComponent.vue';
@ -171,6 +195,8 @@ import BandwidthChart from '@/components/project/dashboard/BandwidthChart.vue';
import StorageChart from '@/components/project/dashboard/StorageChart.vue';
import BucketsDataTable from '@poc/components/BucketsDataTable.vue';
import EditProjectLimitDialog from '@poc/components/dialogs/EditProjectLimitDialog.vue';
import CreateBucketDialog from '@poc/components/dialogs/CreateBucketDialog.vue';
import ManagePassphraseDialog from '@poc/components/dialogs/ManagePassphraseDialog.vue';
import IconCloud from '@poc/components/icons/IconCloud.vue';
import IconArrowDown from '@poc/components/icons/IconArrowDown.vue';
@ -185,10 +211,14 @@ const bucketsStore = useBucketsStore();
const notify = useNotify();
const router = useRouter();
const bucketWasCreated = !!LocalData.getBucketWasCreatedStatus();
const chartWidth = ref<number>(0);
const chartContainer = ref<ComponentPublicInstance>();
const isEditLimitDialogShown = ref<boolean>(false);
const limitToChange = ref<LimitToChange>(LimitToChange.Storage);
const isCreateBucketDialogShown = ref<boolean>(false);
const isSetPassphraseDialogShown = ref<boolean>(false);
/**
* Returns percent of coupon used.
@ -374,6 +404,13 @@ const allocatedBandwidthUsage = computed((): DataStamp[] => {
);
});
/**
* Indicates if user should be prompted to enter the project passphrase.
*/
const promptForPassphrase = computed((): boolean => {
return bucketsStore.state.promptForPassphrase;
});
/**
* Used container size recalculation for charts resizing.
*/

View File

@ -136,6 +136,7 @@ import {
VForm,
VCardActions,
VTextField,
VAlert,
} from 'vuetify/components';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
@ -162,7 +163,7 @@ const dialog = ref<boolean>(false);
const valid = ref<boolean>(false);
const email = ref<string>('');
const selectedMembers = ref<string[]>([]);
const tableComponent = ref<TeamTableComponent & DeleteDialog>();
const tableComponent = ref<InstanceType<typeof TeamTableComponent> & DeleteDialog>();
const emailRules = [
(value: string): string | boolean => (!!value || 'E-mail is requred.'),
@ -207,6 +208,6 @@ async function onAddUsersClick(): Promise<void> {
* Makes delete project members dialog visible.
*/
function showDeleteDialog(): void {
tableComponent.value.showDeleteDialog();
tableComponent.value?.showDeleteDialog();
}
</script>