Skip to content

Commit

Permalink
web/satellite/vuetify-poc: notifications for getting close to limits
Browse files Browse the repository at this point in the history
Added notification banners on project dashboard when user is close to or reached some particular project limit.
Implementation is similar to main app notifications.

Issue:
#6459

Change-Id: Ifaf14facabd0b57f45431c874cfd6fcc1e991282
  • Loading branch information
VitaliiShpital authored and Storj Robot committed Nov 10, 2023
1 parent 98234e7 commit 147076a
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 2 deletions.
175 changes: 175 additions & 0 deletions web/satellite/vuetify-poc/src/components/LimitWarningBanners.vue
@@ -0,0 +1,175 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<v-alert
v-for="threshold in activeThresholds"
:key="threshold"
closable
variant="tonal"
:title="bannerText[threshold].title"
:text="bannerText[threshold].message"
:type="isHundred(threshold) ? 'error' : 'warning'"
rounded="lg"
class="my-2"
border
/>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { VAlert } from 'vuetify/components';
import { useRouter } from 'vue-router';
import { LimitThreshold, LimitThresholdsReached, LimitType } from '@/types/projects';
import { useUsersStore } from '@/store/modules/usersStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { humanizeArray } from '@/utils/strings';
import { Memory } from '@/utils/bytesSize';
import { DEFAULT_PROJECT_LIMITS, useProjectsStore } from '@/store/modules/projectsStore';
type BannerText = {
title: string;
message: string;
};
const projectsStore = useProjectsStore();
const usersStore = useUsersStore();
const configStore = useConfigStore();
const analyticsStore = useAnalyticsStore();
const router = useRouter();
/**
* Returns which limit thresholds have been reached by which usage limit type.
*/
const reachedThresholds = computed((): LimitThresholdsReached => {
const reached: LimitThresholdsReached = {
Eighty: [],
Hundred: [],
CustomEighty: [],
CustomHundred: [],
};
const currentLimits = projectsStore.state.currentLimits;
const config = configStore.state.config;
if (isAccountFrozen.value || currentLimits === DEFAULT_PROJECT_LIMITS) return reached;
type LimitInfo = {
used: number;
currentLimit: number;
paidLimit?: number;
};
const info: Record<LimitType, LimitInfo> = {
Storage: {
used: currentLimits.storageUsed,
currentLimit: currentLimits.storageLimit,
paidLimit: parseConfigLimit(config.defaultPaidStorageLimit),
},
Egress: {
used: currentLimits.bandwidthUsed,
currentLimit: currentLimits.bandwidthLimit,
paidLimit: parseConfigLimit(config.defaultPaidBandwidthLimit),
},
Segment: {
used: currentLimits.segmentUsed,
currentLimit: currentLimits.segmentLimit,
},
};
(Object.entries(info) as [LimitType, LimitInfo][]).forEach(([limitType, info]) => {
const maxLimit = (isPaidTier.value && info.paidLimit) ? Math.max(info.currentLimit, info.paidLimit) : info.currentLimit;
if (info.used >= maxLimit) {
reached.Hundred.push(limitType);
} else if (info.used >= 0.8 * maxLimit) {
reached.Eighty.push(limitType);
} else if (isPaidTier.value) {
if (info.used >= info.currentLimit) {
reached.CustomHundred.push(limitType);
} else if (info.used >= 0.8 * info.currentLimit) {
reached.CustomEighty.push(limitType);
}
}
});
return reached;
});
/**
* Indicates if account was frozen due to billing issues.
*/
const isAccountFrozen = computed<boolean>(() => usersStore.state.user.freezeStatus.frozen);
/**
* Returns the limit thresholds that have been reached by at least 1 usage type.
*/
const activeThresholds = computed<LimitThreshold[]>(() => {
return (Object.keys(LimitThreshold) as LimitThreshold[]).filter(t => reachedThresholds.value[t].length);
});
/**
* Returns whether user is in the paid tier.
*/
const isPaidTier = computed<boolean>(() => {
return usersStore.state.user.paidTier;
});
/**
* Returns banner title and message.
*/
const bannerText = computed<Record<LimitThreshold, BannerText>>(() => {
const record = {} as Record<LimitThreshold, BannerText>;
(Object.keys(LimitThreshold) as LimitThreshold[]).forEach(thresh => {
let limitText = humanizeArray(reachedThresholds.value[thresh]).toLowerCase() + ' limit';
if (reachedThresholds.value[thresh].length > 1) limitText += 's';
const custom = isCustom(thresh);
const hundred = isHundred(thresh);
const title = hundred
? `URGENT: You've reached the ${limitText} for your project.`
: `You've used 80% of your ${limitText}.`;
let message: string;
if (!isPaidTier.value) {
message = hundred
? 'Upgrade to avoid any service interruptions.'
: 'Avoid interrupting your usage by upgrading your account.';
} else {
message = custom
? 'You can increase your limits here or in the Project Settings page.'
: 'Contact support to avoid any service interruptions.';
}
record[thresh] = { title, message };
});
return record;
});
/**
* Parses limit value from config, returning it as a byte amount.
*/
function parseConfigLimit(limit: string): number {
const [value, unit] = limit.split(' ');
return parseFloat(value) * Memory[unit === 'B' ? 'Bytes' : unit];
}
/**
* Returns whether the threshold represents 100% usage.
*/
function isHundred(threshold: LimitThreshold): boolean {
return threshold.toLowerCase().includes('hundred');
}
/**
* Returns whether the threshold is for a custom limit.
*/
function isCustom(threshold: LimitThreshold): boolean {
return threshold.toLowerCase().includes('custom');
}
</script>
6 changes: 4 additions & 2 deletions web/satellite/vuetify-poc/src/views/Dashboard.vue
Expand Up @@ -3,7 +3,8 @@

<template>
<v-container>
<v-row v-if="promptForPassphrase && !bucketWasCreated" class="mt-10 mb-15">
<limit-warning-banners v-if="billingEnabled" />
<v-row v-if="promptForPassphrase && !bucketWasCreated" class="my-0">
<v-col cols="12">
<p class="text-h5 font-weight-bold">Set an encryption passphrase<br>to start uploading files.</p>
</v-col>
Expand All @@ -13,7 +14,7 @@
</v-btn>
</v-col>
</v-row>
<v-row v-else-if="!promptForPassphrase && !bucketWasCreated && !bucketsCount" class="mt-10 mb-15">
<v-row v-else-if="!promptForPassphrase && !bucketWasCreated && !bucketsCount" class="my-0">
<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>
Expand Down Expand Up @@ -224,6 +225,7 @@ 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';
import LimitWarningBanners from '@poc/components/LimitWarningBanners.vue';
const appStore = useAppStore();
const usersStore = useUsersStore();
Expand Down

0 comments on commit 147076a

Please sign in to comment.