satellite/{web, console}: implemented new objects uploading hover component

Added new feature flag.
Implemented new objects uploading hover component.

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

Change-Id: If43ade5db33aecba6c089ce727503518c59f6ffc
This commit is contained in:
Vitalii 2023-05-19 17:24:59 +03:00 committed by Storj Robot
parent d8f64326f5
commit 98591f34f9
20 changed files with 850 additions and 100 deletions

View File

@ -45,6 +45,7 @@ type FrontendConfig struct {
PasswordMaximumLength int `json:"passwordMaximumLength"`
ABTestingEnabled bool `json:"abTestingEnabled"`
PricingPackagesEnabled bool `json:"pricingPackagesEnabled"`
NewUploadModalEnabled bool `json:"newUploadModalEnabled"`
}
// Satellites is a configuration value that contains a list of satellite names and addresses.

View File

@ -98,6 +98,7 @@ type Config struct {
HomepageURL string `help:"url link to storj.io homepage" default:"https://www.storj.io"`
NativeTokenPaymentsEnabled bool `help:"indicates if storj native token payments system is enabled" default:"false"`
PricingPackagesEnabled bool `help:"whether to allow purchasing pricing packages" default:"false" devDefault:"true"`
NewUploadModalEnabled bool `help:"whether to show new upload modal" default:"false"`
OauthCodeExpiry time.Duration `help:"how long oauth authorization codes are issued for" default:"10m"`
OauthAccessTokenExpiry time.Duration `help:"how long oauth access tokens are issued for" default:"24h"`
@ -540,6 +541,7 @@ func (server *Server) frontendConfigHandler(w http.ResponseWriter, r *http.Reque
PasswordMaximumLength: console.PasswordMaximumLength,
ABTestingEnabled: server.config.ABTesting.Enabled,
PricingPackagesEnabled: server.config.PricingPackagesEnabled,
NewUploadModalEnabled: server.config.NewUploadModalEnabled,
}
err := json.NewEncoder(w).Encode(&cfg)

View File

@ -283,6 +283,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# indicates if storj native token payments system is enabled
# console.native-token-payments-enabled: false
# whether to show new upload modal
# console.new-upload-modal-enabled: false
# how long oauth access tokens are issued for
# console.oauth-access-token-expiry: 24h0m0s

View File

@ -100,6 +100,7 @@
<file-browser-header />
</template>
<template #body>
<template v-if="!isNewUploadingModal">
<tr
v-for="(file, index) in formattedFilesUploading"
:key="index"
@ -149,6 +150,7 @@
<th class="hide-mobile files-uploading-count__content" />
<th class="files-uploading-count__content" />
</tr>
</template>
<up-entry v-if="path.length > 0" :on-back="onBack" />
@ -193,7 +195,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, reactive, ref } from 'vue';
import { computed, onBeforeMount, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import FileBrowserHeader from './FileBrowserHeader.vue';
@ -210,6 +212,7 @@ import { MODALS } from '@/utils/constants/appStatePopUps';
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useAppStore } from '@/store/modules/appStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useConfigStore } from '@/store/modules/configStore';
import VButton from '@/components/common/VButton.vue';
import BucketSettingsNav from '@/components/objects/BucketSettingsNav.vue';
@ -224,6 +227,7 @@ import UploadIcon from '@/../static/images/browser/upload.svg';
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const obStore = useObjectBrowserStore();
const configStore = useConfigStore();
const router = useRouter();
const route = useRoute();
@ -250,6 +254,13 @@ const isInitialized = computed((): boolean => {
return obStore.isInitialized;
});
/**
* Indicates if new objects uploading flow should be working.
*/
const isNewUploadingModal = computed((): boolean => {
return configStore.state.config.newUploadModalEnabled;
});
/**
* Retrieve the current path from the store.
*/

View File

@ -10,7 +10,9 @@
:item="{'name': 'Objects locked', 'size': '', 'date': ''}"
item-type="locked"
>
<th slot="options" />
<template #options>
<th />
</template>
</table-item>
</template>

View File

@ -9,7 +9,9 @@
:item="{'name': 'Back', 'size': '', 'date': ''}"
item-type="back"
>
<th slot="options" />
<template #options>
<th />
</template>
</table-item>
</template>

View File

@ -0,0 +1,288 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="modal">
<div class="modal__header" :class="{'custom-radius': !isExpanded}" @click="toggleExpanded">
<div class="modal__header__left">
<div class="modal__header__left__info">
<p class="modal__header__left__info__title">{{ statusLabel }}</p>
<p class="modal__header__left__info__remaining">{{ remainingTimeString }}</p>
</div>
<div v-if="!isClosable" class="modal__header__left__track">
<div class="modal__header__left__track__fill" :style="progressStyle" />
</div>
</div>
<div class="modal__header__right">
<ArrowIcon class="modal__header__right__arrow" :class="{rotated: isExpanded}" />
<CloseIcon v-if="isClosable" class="modal__header__right__close" @click="closeModal" />
</div>
</div>
<div v-if="isExpanded" class="modal__items">
<div v-for="item in uploading" :key="item.Key">
<UploadItem :item="item" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { UploadingBrowserObject, UploadingStatus, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useAppStore } from '@/store/modules/appStore';
import { Duration } from '@/utils/time';
import UploadItem from '@/components/modals/objectUpload/UploadItem.vue';
import ArrowIcon from '@/../static/images/modals/objectUpload/arrow.svg';
import CloseIcon from '@/../static/images/modals/objectUpload/close.svg';
const obStore = useObjectBrowserStore();
const appStore = useAppStore();
const isExpanded = ref<boolean>(false);
const startDate = ref<number>(Date.now());
const remainingTimeString = ref<string>('');
const interval = ref<NodeJS.Timer>();
/**
* Returns uploading objects from store.
*/
const uploading = computed((): UploadingBrowserObject[] => {
return obStore.state.uploading;
});
/**
* Returns uploading objects with InProgress status.
*/
const objectsInProgress = computed((): UploadingBrowserObject[] => {
return uploading.value.filter(f => f.status === UploadingStatus.InProgress);
});
/**
* Indicates if modal is closable.
*/
const isClosable = computed((): boolean => {
return !objectsInProgress.value.length;
});
/**
* Returns header's status label.
*/
const statusLabel = computed((): string => {
if (isClosable.value) {
let status = 'Uploading completed';
const failedUploads = uploading.value.filter(f => f.status === UploadingStatus.Failed);
if (failedUploads.length > 0) {
status += ` (${failedUploads.length} failed`;
}
const cancelledUploads = uploading.value.filter(f => f.status === UploadingStatus.Cancelled);
if (cancelledUploads.length > 0) {
status += `, (${cancelledUploads.length} cancelled`;
}
if (!failedUploads.length && !cancelledUploads.length) {
return status;
}
return `${status})`;
}
if (uploading.value.length === 1) {
return 'Uploading 1 item';
}
return `Uploading ${uploading.value.length} items`;
});
/**
* Returns progress bar style.
*/
const progressStyle = computed((): Record<string, string> => {
const progress = uploading.value.reduce((total: number, item: UploadingBrowserObject) => {
total += item.progress || 0;
return total;
}, 0) / uploading.value.length;
return {
width: `${progress}%`,
};
});
/**
* Calculates remaining seconds.
*/
function calculateRemainingTime(): void {
const progress = uploading.value.reduce((total: number, item: UploadingBrowserObject) => {
if (item.progress && item.progress !== 100) {
total += item.progress;
}
return total;
}, 0);
const remainingProgress = 100 - progress;
const averageProgressPerNanosecond = progress / ((Date.now() - startDate.value) * 1000000);
const remainingNanoseconds = remainingProgress / averageProgressPerNanosecond;
if (!isFinite(remainingNanoseconds) || remainingNanoseconds < 0) {
return;
}
remainingTimeString.value = new Duration(remainingNanoseconds).remainingFormatted;
}
/**
* Toggles expanded state.
*/
function toggleExpanded(): void {
isExpanded.value = !isExpanded.value;
}
/**
* Closes modal.
*/
function closeModal(): void {
obStore.clearUploading();
appStore.setUploadingModal(false);
}
/**
* Starts interval for recalculating remaining time.
*/
function startInterval(): void {
const int = setInterval(() => {
if (isClosable.value) {
clearInterval(int);
interval.value = undefined;
remainingTimeString.value = '';
return;
}
calculateRemainingTime();
}, 2000); // recalculate every 2 seconds.
interval.value = int;
}
watch(() => objectsInProgress.value.length, () => {
if (!interval.value) {
startDate.value = Date.now();
startInterval();
}
});
onMounted(() => {
startInterval();
});
</script>
<style scoped lang="scss">
.modal {
position: fixed;
right: 24px;
bottom: 24px;
width: 500px;
max-width: 500px;
border-radius: 8px;
font-family: 'font_regular', sans-serif;
@media screen and (width <= 650px) {
max-width: unset;
width: unset;
left: 126px;
}
@media screen and (width <= 500px) {
left: 24px;
}
&__header {
background-color: var(--c-grey-10);
padding: 16px;
cursor: pointer;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
&__left {
box-sizing: border-box;
margin-right: 24px;
width: 100%;
&__info {
display: flex;
align-items: center;
justify-content: space-between;
&__title {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 20px;
color: var(--c-white);
}
&__remaining {
font-size: 14px;
line-height: 20px;
color: var(--c-white);
opacity: 0.7;
}
}
&__track {
margin-top: 10px;
width: 100%;
height: 8px;
border-radius: 4px;
position: relative;
background-color: var(--c-grey-11);
&__fill {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: var(--c-blue-1);
border-radius: 4px;
max-width: 100%;
}
}
}
&__right {
display: flex;
align-items: center;
&__arrow {
transition: all 0.3s ease-out;
}
&__close {
margin-left: 30px;
:deep(path) {
fill: var(--c-white);
}
}
}
}
&__items {
border: 1px solid var(--c-grey-3);
border-radius: 0 0 8px 8px;
max-height: 185px;
overflow-y: auto;
}
}
.rotated {
transform: rotate(180deg);
}
.custom-radius {
border-radius: 8px;
}
</style>

View File

@ -0,0 +1,277 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="item">
<div class="item__left">
<div class="item__left__icon">
<p class="item__left__icon__label">{{ extension }}</p>
</div>
<p class="item__left__name" :title="item.Key">{{ item.Key }}</p>
</div>
<div class="item__right">
<template v-if="item.status === UploadingStatus.InProgress">
<div class="item__right__track">
<div class="item__right__track__fill" :style="progressStyle" />
</div>
<CloseIcon class="item__right__cancel" @click="cancelUpload" />
</template>
<p v-if="item.status === UploadingStatus.Cancelled" class="item__right__cancelled">Upload cancelled</p>
<CheckIcon v-if="item.status === UploadingStatus.Finished" />
<template v-if="item.status === UploadingStatus.Failed">
<p class="item__right__failed">{{ item.failedMessage }}</p>
<FailedIcon />
<VInfo v-if="item.failedMessage === FailedUploadMessage.TooBig" class="item__right__info">
<template #icon>
<InfoIcon />
</template>
<template #message>
<p class="item__right__info__message">
Use Command Line Interface to drop files more than 30 GB.
<a
class="item__right__info__message__link"
href="https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/prerequisites"
target="_blank"
rel="noopener noreferrer"
>
More information
</a>
</p>
</template>
</VInfo>
<p v-else class="item__right__retry" @click="retryUpload">Retry</p>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
UploadingBrowserObject,
UploadingStatus,
FailedUploadMessage,
useObjectBrowserStore,
} from '@/store/modules/objectBrowserStore';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import VInfo from '@/components/common/VInfo.vue';
import CloseIcon from '@/../static/images/modals/objectUpload/close.svg';
import CheckIcon from '@/../static/images/modals/objectUpload/check.svg';
import FailedIcon from '@/../static/images/modals/objectUpload/failed.svg';
import InfoIcon from '@/../static/images/modals/objectUpload/info.svg';
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const obStore = useObjectBrowserStore();
const notify = useNotify();
const props = defineProps<{
item: UploadingBrowserObject
}>();
/**
* Returns file's extension.
*/
const extension = computed((): string => {
return props.item.Key.split('.').pop()?.substring(0, 3).toUpperCase() || 'EXT';
});
/**
* Returns progress bar style.
*/
const progressStyle = computed((): Record<string, string> => {
return {
width: props.item.progress ? `${props.item.progress}%` : '0%',
};
});
/**
* Retries failed upload.
*/
async function retryUpload(): Promise<void> {
try {
await obStore.retryUpload(props.item);
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.OBJECTS_UPLOAD_MODAL);
}
}
/**
* Cancels active upload.
*/
function cancelUpload(): void {
try {
obStore.cancelUpload(props.item.Key);
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.OBJECTS_UPLOAD_MODAL);
}
}
</script>
<style scoped lang="scss">
.item {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--c-grey-3);
padding: 14px 20px;
font-family: 'font_regular', sans-serif;
background-color: var(--c-white);
@media screen and (width <= 450px) {
padding: 14px;
}
&:last-of-type {
border-radius: 0 0 8px 8px;
}
&__left {
display: flex;
align-items: center;
max-width: 56%;
@media screen and (width <= 450px) {
max-width: 40%;
}
&__icon {
min-width: 32px;
width: 32px;
height: 32px;
background-color: var(--c-green-6);
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
@media screen and (width <= 550px) {
display: none;
}
&__label {
font-family: 'font_bold', sans-serif;
font-size: 9px;
line-height: 18px;
color: var(--c-green-5);
}
}
&__name {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 20px;
color: var(--c-grey-9);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
&__right {
display: flex;
align-items: center;
margin-left: 20px;
svg {
min-width: 20px;
}
&__track {
min-width: 130px;
height: 6px;
border-radius: 3px;
position: relative;
margin-right: 34px;
background-color: var(--c-blue-1);
&__fill {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: var(--c-blue-3);
border-radius: 3px;
max-width: 100%;
}
}
&__cancel {
cursor: pointer;
}
&__cancelled {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 20px;
color: var(--c-grey-5);
}
&__failed {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 20px;
color: var(--c-red-4);
margin-right: 8px;
}
&__retry {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 20px;
color: var(--c-blue-3);
margin-left: 18px;
cursor: pointer;
}
&__info {
cursor: pointer;
max-height: 20px;
margin-left: 18px;
&__message {
font-size: 14px;
line-height: 20px;
text-align: center;
color: var(--c-black);
&__link {
color: var(--c-blue-3);
&:visited {
color: var(--c-blue-3);
}
}
}
}
}
}
:deep(.info__box) {
width: 290px;
left: calc(50% - 265px);
top: calc(100% - 85px);
cursor: default;
filter: none;
transform: rotate(-180deg);
}
:deep(.info__box__message) {
border-radius: 4px;
padding: 10px 8px;
transform: rotate(-180deg);
border: 1px solid var(--c-grey-5);
}
:deep(.info__box__arrow) {
width: 10px;
height: 10px;
margin-bottom: -3px;
}
</style>

View File

@ -26,6 +26,7 @@ class AppState {
public managePassphraseStep: ManageProjectPassphraseStep | undefined = undefined;
public activeDropdown = 'none';
public activeModal: Component | null = null;
public isUploadingModal = false;
// this field is mainly used on the all projects dashboard as an exit condition
// for when the dashboard opens the pricing plan and the pricing plan navigates back repeatedly.
public hasShownPricingPlan = false;
@ -99,6 +100,10 @@ export const useAppStore = defineStore('app', () => {
state.onbApiKey = apiKey;
}
function setUploadingModal(value: boolean): void {
state.isUploadingModal = value;
}
function setOnboardingCleanAPIKey(apiKey: string): void {
state.onbCleanApiKey = apiKey;
}
@ -184,6 +189,7 @@ export const useAppStore = defineStore('app', () => {
setPricingPlan,
setManagePassphraseStep,
setHasShownPricingPlan,
setUploadingModal,
setLargeUploadWarningNotification,
setLargeUploadNotification,
closeDropdowns,

View File

@ -9,6 +9,7 @@ import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { useConfigStore } from '@/store/modules/configStore';
const listCache = new Map();
@ -26,6 +27,24 @@ export type BrowserObject = {
path?: string;
};
export enum FailedUploadMessage {
Failed = 'Upload failed',
TooBig = 'File is too big',
}
export enum UploadingStatus {
InProgress,
Finished,
Failed,
Cancelled,
}
export type UploadingBrowserObject = BrowserObject & {
status: UploadingStatus;
Body: File;
failedMessage?: FailedUploadMessage;
}
export class FilesState {
s3: S3 | null = null;
accessKey: null | string = null;
@ -34,7 +53,7 @@ export class FilesState {
browserRoot = '/';
files: BrowserObject[] = [];
uploadChain: Promise<void> = Promise.resolve();
uploading: BrowserObject[] = [];
uploading: UploadingBrowserObject[] = [];
selectedAnchorFile: BrowserObject | null = null;
unselectedAnchorFile: BrowserObject | null = null;
selectedFiles: BrowserObject[] = [];
@ -110,6 +129,12 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
});
const uploadingLength = computed(() => {
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
return state.uploading.filter(f => f.status === UploadingStatus.InProgress).length;
}
return state.uploading.length;
});
@ -298,7 +323,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
: [];
async function* traverse(item: Item | Item[], path = '') {
if ('isFile' in item && item.isFile === true) {
if ('isFile' in item && item.isFile) {
const file = await new Promise(item.file.bind(item));
yield { path, file };
} else if (item instanceof File) {
@ -329,7 +354,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
path + item.name + '/',
);
}
} else if ('length' in item && typeof item.length === 'number') {
} else if ('length' in item) {
for (const i of item) {
yield* traverse(i);
}
@ -363,6 +388,8 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
}
const appStore = useAppStore();
const config = useConfigStore();
const { notifyError } = useNotificationsStore();
for await (const { path, file } of traverse(iterator)) {
const directories = path.split('/');
@ -376,6 +403,13 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Body: file,
};
if (config.state.config.newUploadModalEnabled) {
if (state.uploading.some(f => f.Key === params.Key && f.status === UploadingStatus.InProgress)) {
notifyError({ message: `${params.Key} is already uploading`, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
continue;
}
}
// If file size exceeds 1 GB, show warning notification
if (file.size > (1024 * 1024 * 1024)) {
appStore.setLargeUploadWarningNotification(true);
@ -387,59 +421,145 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
);
upload.on('httpUploadProgress', async (progress) => {
const file = state.uploading.find((file) => file.Key === params.Key);
if (file === undefined) {
const file = state.uploading.find(file => file.Key === params.Key);
if (!file) {
throw new Error(`No file found with key ${JSON.stringify(params.Key)}`);
}
file.progress = Math.round((progress.loaded / progress.total) * 100);
});
if (config.state.config.newUploadModalEnabled) {
if (state.uploading.some(f => f.Key === params.Key && f.status === UploadingStatus.Cancelled)) {
state.uploading = state.uploading.filter(f => f.Key !== params.Key);
}
// If file size exceeds 30 GB, abort the upload attempt
if (file.size > (30 * 1024 * 1024 * 1024)) {
state.uploading.push({
...params,
upload,
progress: 0,
Size: 0,
LastModified: 0,
Body: file,
status: UploadingStatus.Failed,
failedMessage: FailedUploadMessage.TooBig,
});
appStore.setUploadingModal(true);
continue;
}
}
state.uploading.push({
...params,
upload,
progress: 0,
Size: 0,
LastModified: 0,
status: UploadingStatus.InProgress,
});
if (config.state.config.newUploadModalEnabled && !appStore.state.isUploadingModal) {
appStore.setUploadingModal(true);
}
state.uploadChain = state.uploadChain.then(async () => {
if (
state.uploading.findIndex((file) => file.Key === params.Key) === -1
) {
const index = state.uploading.findIndex(f => f.Key === params.Key);
if (index === -1) {
// upload cancelled or removed
return;
}
try {
await upload.promise();
state.uploading[index].status = UploadingStatus.Finished;
} catch (error) {
const { notifyError } = useNotificationsStore();
const limitExceededError = 'storage limit exceeded';
if (error.message.includes(limitExceededError)) {
notifyError({ message: `Error: ${limitExceededError}`, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
} else {
notifyError({ message: error.message, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
}
handleUploadError(error.message, index);
return;
}
await list();
const uploadedFiles = state.files.filter(
(file) => file.type === 'file',
);
const uploadedFiles = state.files.filter(f => f.type === 'file');
if (uploadedFiles.length === 1 && !path && state.openModalOnFirstUpload) {
state.objectPathForModal = params.Key;
appStore.updateActiveModal(MODALS.objectDetails);
}
if (!config.state.config.newUploadModalEnabled) {
state.uploading = state.uploading.filter((file) => file.Key !== params.Key);
}
});
}
}
async function retryUpload(item: UploadingBrowserObject): Promise<void> {
assertIsInitialized(state);
const index = state.uploading.findIndex(f => f.Key === item.Key);
if (index === -1) {
throw new Error(`No file found with key ${JSON.stringify(item.Key)}`);
}
const params = {
Bucket: state.bucket,
Key: item.Key,
Body: item.Body,
};
const upload = state.s3.upload(
{ ...params },
{ partSize: 64 * 1024 * 1024 },
);
upload.on('httpUploadProgress', async (progress) => {
const file = state.uploading.find(file => file.Key === params.Key);
if (!file) {
throw new Error(`No file found with key ${JSON.stringify(params.Key)}`);
}
file.progress = Math.round((progress.loaded / progress.total) * 100);
});
state.uploading[index] = {
...params,
upload,
progress: 0,
Size: 0,
LastModified: 0,
status: UploadingStatus.InProgress,
};
try {
await upload.promise();
state.uploading[index].status = UploadingStatus.Finished;
} catch (error) {
handleUploadError(error.message, index);
}
await list();
}
function handleUploadError(message: string, index: number): void {
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
state.uploading[index].status = UploadingStatus.Failed;
state.uploading[index].failedMessage = FailedUploadMessage.Failed;
}
const { notifyError } = useNotificationsStore();
const limitExceededError = 'storage limit exceeded';
if (message.includes(limitExceededError)) {
notifyError({ message: `Error: ${limitExceededError}`, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
} else {
notifyError({ message, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
}
}
async function createFolder(name): Promise<void> {
assertIsInitialized(state);
@ -467,6 +587,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
})
.promise();
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
state.uploading = state.uploading.filter(f => f.Key !== file.Key);
}
if (!isFolder) {
await list();
removeFileFromToBeDeleted(file);
@ -582,7 +707,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
function removeFileFromToBeDeleted(file): void {
state.filesToBeDeleted = state.filesToBeDeleted.filter(
(singleFile) => singleFile.Key !== file.Key,
singleFile => singleFile.Key !== file.Key,
);
}
@ -609,16 +734,15 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
}
function cancelUpload(key): void {
const file = state.uploading.find((file) => file.Key === key);
if (typeof file === 'object') {
if (file.progress !== undefined && file.upload && file.progress > 0) {
file.upload.abort();
const index = state.uploading.findIndex(f => f.Key === key);
if (index === -1) {
throw new Error(`File ${JSON.stringify(key)} not found`);
}
state.uploading = state.uploading.filter((file) => file.Key !== key);
} else {
throw new Error(`File ${JSON.stringify(key)} not found`);
const file = state.uploading[index];
if (file.progress !== undefined && file.upload && file.progress > 0) {
file.upload.abort();
state.uploading[index].status = UploadingStatus.Cancelled;
}
}
@ -641,14 +765,8 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
state.unselectedAnchorFile = file;
}
function closeAllInteractions(): void {
if (state.openedDropdown) {
closeDropdown();
}
if (state.selectedAnchorFile) {
clearAllSelectedFiles();
}
function clearUploading(): void {
state.uploading = [];
}
function clear(): void {
@ -681,12 +799,12 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
uploadingLength,
init,
reinit,
updateFiles,
list,
back,
sort,
getObjectCount,
upload,
retryUpload,
createFolder,
deleteObject,
deleteFolder,
@ -704,7 +822,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
setSelectedAnchorFile,
setUnselectedAnchorFile,
cancelUpload,
closeAllInteractions,
clearUploading,
clear,
};
});

View File

@ -31,8 +31,6 @@ export class FrontendConfig {
allProjectsDashboard: boolean;
defaultPaidStorageLimit: MemorySize;
defaultPaidBandwidthLimit: MemorySize;
newBillingScreen: boolean;
newAccessGrantFlow: boolean;
inactivityTimerEnabled: boolean;
inactivityTimerDuration: number;
inactivityTimerViewerEnabled: boolean;
@ -43,6 +41,7 @@ export class FrontendConfig {
passwordMaximumLength: number;
abTestingEnabled: boolean;
pricingPackagesEnabled: boolean;
newUploadModalEnabled: boolean;
}
export class MultiCaptchaConfig {

View File

@ -4,15 +4,12 @@
// Make sure these event names match up with the client-side event names in satellite/analytics/service.go
export enum AnalyticsEvent {
GATEWAY_CREDENTIALS_CREATED = 'Credentials Created',
PASSPHRASE_CREATED = 'Passphrase Created',
EXTERNAL_LINK_CLICKED = 'External Link Clicked',
PATH_SELECTED = 'Path Selected',
LINK_SHARED = 'Link Shared',
OBJECT_UPLOADED = 'Object Uploaded',
API_KEY_GENERATED = 'API Key Generated',
UPGRADE_BANNER_CLICKED = 'Upgrade Banner Clicked',
MODAL_ADD_CARD = 'Credit Card Added In Modal',
MODAL_ADD_TOKENS = 'Storj Token Added In Modal',
SEARCH_BUCKETS = 'Search Buckets',
NAVIGATE_PROJECTS = 'Navigate Projects',
MANAGE_PROJECTS_CLICKED = 'Manage Projects Clicked',
@ -34,23 +31,19 @@ export enum AnalyticsEvent {
API_ACCESS_CREATED = 'API Access Created',
UPLOAD_FILE_CLICKED = 'Upload File Clicked',
UPLOAD_FOLDER_CLICKED = 'Upload Folder Clicked',
CREATE_KEYS_CLICKED = 'Create Keys Clicked',
DOWNLOAD_TXT_CLICKED = 'Download txt clicked',
ENCRYPT_MY_ACCESS_CLICKED = 'Encrypt My Access Clicked',
COPY_TO_CLIPBOARD_CLICKED = 'Copy to Clipboard Clicked',
CREATE_ACCESS_GRANT_CLICKED = 'Create Access Grant Clicked',
CREATE_S3_CREDENTIALS_CLICKED = 'Create S3 Credentials Clicked',
CREATE_KEYS_FOR_CLI_CLICKED = 'Create Keys For CLI Clicked',
SEE_PAYMENTS_CLICKED = 'See Payments Clicked',
EDIT_PAYMENT_METHOD_CLICKED = 'Edit Payment Method Clicked',
USAGE_DETAILED_INFO_CLICKED = 'Usage Detailed Info Clicked',
ADD_NEW_PAYMENT_METHOD_CLICKED = 'Add New Payment Method Clicked',
APPLY_NEW_COUPON_CLICKED = 'Apply New Coupon Clicked',
CREDIT_CARD_REMOVED = 'Credit Card Removed',
COUPON_CODE_APPLIED = 'Coupon Code Applied',
INVOICE_DOWNLOADED = 'Invoice Downloaded',
CREDIT_CARD_ADDED_FROM_BILLING = 'Credit Card Added From Billing',
STORJ_TOKEN_ADDED_FROM_BILLING = 'Storj Token Added From Billing',
ADD_FUNDS_CLICKED = 'Add Funds Clicked',
PROJECT_MEMBERS_INVITE_SENT = 'Project Members Invite Sent',
UI_ERROR = 'UI error occurred',
@ -91,6 +84,7 @@ export enum AnalyticsErrorEventSource {
OBJECT_DETAILS_MODAL = 'Object details modal',
OPEN_BUCKET_MODAL = 'Open bucket modal',
SHARE_BUCKET_MODAL = 'Share bucket modal',
OBJECTS_UPLOAD_MODAL = 'Objects upload modal',
NAVIGATION_ACCOUNT_AREA = 'Navigation account area',
NAVIGATION_PROJECT_SELECTION = 'Navigation project selection',
MOBILE_NAVIGATION = 'Mobile navigation',

View File

@ -18,7 +18,6 @@ export class Time {
* This class simplifies working with duration (nanoseconds) sent from the backend.
* */
export class Duration {
static MINUTES_15 = new Duration(9e+11);
static MINUTES_30 = new Duration(1.8e+12);
static HOUR_1 = new Duration(3.6e+12);
@ -90,6 +89,24 @@ export class Duration {
return `${numberPart} ${unitPart}`;
}
get remainingFormatted(): string {
const seconds = Math.floor(this.nanoseconds / 1000000000);
const remainingHours = Math.floor(seconds / 3600);
const remainingMinutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60);
let timeString = '';
if (remainingHours > 0) {
timeString += `${remainingHours}h `;
}
if (remainingMinutes > 0) {
timeString += `${remainingMinutes}m `;
}
timeString += `${remainingSeconds}s`;
return timeString;
}
public isEqualTo(other: Duration): boolean {
return this.nanoseconds === other.nanoseconds;
}

View File

@ -120,6 +120,7 @@
:on-upgrade="togglePMModal"
/>
<AllModals />
<ObjectsUploadingModal v-if="isObjectsUploadModal" />
<!-- IMPORTANT! Make sure these 2 modals are positioned as the last elements here so that they are shown on top of everything else -->
<InactivityModal
v-if="inactivityModalShown"
@ -174,6 +175,7 @@ import UpgradeNotification from '@/components/notifications/UpgradeNotification.
import ProjectLimitBanner from '@/components/notifications/ProjectLimitBanner.vue';
import BrandedLoader from '@/components/common/BrandedLoader.vue';
import UpdateSessionTimeoutBanner from '@/components/notifications/UpdateSessionTimeoutBanner.vue';
import ObjectsUploadingModal from '@/components/modals/objectUpload/ObjectsUploadingModal.vue';
import CloudIcon from '@/../static/images/notifications/cloudAlert.svg';
import WarningIcon from '@/../static/images/notifications/circleWarning.svg';
@ -233,6 +235,13 @@ const sessionRefreshInterval = computed((): number => {
return sessionDuration.value / 2;
});
/**
* Indicates whether objects upload modal should be shown.
*/
const isObjectsUploadModal = computed((): boolean => {
return configStore.state.config.newUploadModalEnabled && appStore.state.isUploadingModal;
});
/**
* Indicates whether the update session timeout notification should be shown.
*/

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.46647 5.221C9.75394 4.93352 10.2157 4.92651 10.5116 5.19996L10.5335 5.221L18.779 13.4665C19.0737 13.7611 19.0737 14.2389 18.779 14.5335C18.4915 14.821 18.0298 14.828 17.7338 14.5546L17.7119 14.5335L9.99999 6.8214L2.28806 14.5335C2.00058 14.821 1.53885 14.828 1.24288 14.5546L1.221 14.5335C0.933522 14.2461 0.92651 13.7843 1.19996 13.4884L1.221 13.4665L9.46647 5.221Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.6175 4.39957C19.1151 4.91934 19.1272 5.75418 18.6539 6.28932L18.6175 6.32888L8.83282 16.5491C8.64102 16.7495 8.40546 16.8772 8.15784 16.9319C7.73208 17.0804 7.24386 16.9867 6.89303 16.6439L6.8778 16.6287L1.38254 10.8891C0.872486 10.3563 0.872486 9.49253 1.38254 8.95976C1.88016 8.43999 2.67941 8.42732 3.19174 8.92173L3.22962 8.95976L7.81726 13.7513L16.7704 4.39957C17.2804 3.86681 18.1074 3.86681 18.6175 4.39957Z" fill="#00AC26"/>
</svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7143 1.28565C19.0952 1.66652 19.0952 2.28403 18.7143 2.6649L11.3791 9.99987L18.6517 17.2724C19.0325 17.6533 19.0325 18.2708 18.6517 18.6517C18.2708 19.0325 17.6533 19.0325 17.2724 18.6517L9.99987 11.3791L2.6649 18.7143C2.28403 19.0952 1.66652 19.0952 1.28565 18.7143C0.904783 18.3335 0.904783 17.716 1.28565 17.3351L8.62075 9.99975L1.34835 2.72759C0.967476 2.34673 0.967476 1.72921 1.34835 1.34835C1.72921 0.967476 2.34673 0.967476 2.72759 1.34835L10 8.6205L17.3351 1.28565C17.716 0.904783 18.3335 0.904783 18.7143 1.28565Z" fill="#929FB1"/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM11.25 15C11.25 15.6904 10.6904 16.25 10 16.25C9.30964 16.25 8.75 15.6904 8.75 15C8.75 14.3096 9.30964 13.75 10 13.75C10.6904 13.75 11.25 14.3096 11.25 15ZM10 3.75C9.30964 3.75 8.75 4.30964 8.75 5V10C8.75 10.6904 9.30964 11.25 10 11.25C10.6904 11.25 11.25 10.6904 11.25 10V5C11.25 4.30964 10.6904 3.75 10 3.75Z" fill="#BA0000"/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20ZM10 18.1667C14.5103 18.1667 18.1667 14.5103 18.1667 10C18.1667 5.48967 14.5103 1.83333 10 1.83333C5.48967 1.83333 1.83333 5.48967 1.83333 10C1.83333 14.5103 5.48967 18.1667 10 18.1667ZM9.16667 13.4162V10.4767C9.16667 9.97041 9.57707 9.56 10.0833 9.56C10.5772 9.56 10.9799 9.95063 10.9993 10.4398L11 10.4767V13.3552C11 13.8666 10.5945 14.2858 10.0833 14.3028C9.59364 14.3191 9.18345 13.9354 9.16716 13.4457C9.16683 13.4358 9.16667 13.426 9.16667 13.4162ZM9.16667 6.58283V6.47667C9.16667 5.97041 9.57707 5.56 10.0833 5.56C10.5772 5.56 10.9799 5.95063 10.9993 6.4398L11 6.47667V6.52183C11 7.03323 10.5945 7.45249 10.0833 7.4695C9.59364 7.48579 9.18345 7.10202 9.16716 6.61233C9.16683 6.6025 9.16667 6.59266 9.16667 6.58283Z" fill="#091C45"/>
</svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@ -27,10 +27,12 @@
--c-green-3: #00e366;
--c-green-4: #ccf8e0;
--c-green-5: #00ac26;
--c-green-6: #e6f7ea;
--c-red-1: #ffb0b0;
--c-red-2: #ff1313;
--c-red-3: #ba0000;
--c-red-4: #97334e;
--c-pink-1: #ffe0e7;
--c-pink-2: #ffc0cf;
@ -51,6 +53,9 @@
--c-grey-6: #56606d;
--c-grey-7: #384b65;
--c-grey-8: #1b2533;
--c-grey-9: #111827;
--c-grey-10: #313131;
--c-grey-11: #6b6b6b;
--c-white: #ffffff;
--c-black: #000000;