web/satellite: added limit cards on project dashboard

Added storage, bandwidth, segment, free tier and coupon cards to project dashboard.

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

Change-Id: Iaf974273a34c8ed3faf1974876fa685074d5ae61
This commit is contained in:
Vitalii 2023-05-08 17:31:08 +03:00
parent c2710cc78d
commit b91f72f08a
9 changed files with 509 additions and 6 deletions

View File

@ -19,8 +19,8 @@
<div class="info-step__column__bullets">
<InfoBullet class="info-step__column__bullets__item" title="Projects" info="1 project" />
<InfoBullet class="info-step__column__bullets__item" title="Storage" info="25 GB limit" />
<InfoBullet class="info-step__column__bullets__item" title="Download" info="10,000 segments limit" />
<InfoBullet class="info-step__column__bullets__item" title="Segments" info="1 project" />
<InfoBullet class="info-step__column__bullets__item" title="Download" info="25 GB limit" />
<InfoBullet class="info-step__column__bullets__item" title="Segments" info="10,000 segments limit" />
<InfoBullet class="info-step__column__bullets__item" title="Link Sharing" info="Link sharing with Storj domain" />
</div>
</div>

View File

@ -535,7 +535,7 @@ async function onSaveStorageLimitButtonClick(): Promise<void> {
toggleStorageLimitEditing();
analytics.eventTriggered(AnalyticsEvent.PROJECT_STORAGE_LIMIT_UPDATED);
await notify.success('Project storage limit updated successfully!');
notify.success('Project storage limit updated successfully!');
}
/**
@ -560,7 +560,7 @@ async function onSaveBandwidthLimitButtonClick(): Promise<void> {
toggleBandwidthLimitEditing();
analytics.eventTriggered(AnalyticsEvent.PROJECT_BANDWIDTH_LIMIT_UPDATED);
await notify.success('Project bandwidth limit updated successfully!');
notify.success('Project bandwidth limit updated successfully!');
}
/**

View File

@ -0,0 +1,158 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="card">
<div class="card__header">
<component :is="icon" />
<h2 class="card__header__title">{{ title }}</h2>
</div>
<VLoader v-if="isLoading" />
<template v-else>
<div class="card__track">
<div class="card__track__fill" :style="style" />
</div>
<div class="card__data">
<div>
<h2 class="card__data__title">{{ usedTitle }}</h2>
<p class="card__data__info">{{ usedInfo }}</p>
</div>
<div>
<h2 class="card__data__title">{{ availableTitle }}</h2>
<p v-if="useAction" class="card__data__action" @click="onAction">{{ actionTitle }}</p>
<a
v-else
class="card__data__action"
target="_blank"
rel="noopener noreferrer"
:href="link"
>
{{ actionTitle }}
</a>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, VueConstructor } from 'vue';
import VLoader from '@/components/common/VLoader.vue';
const props = withDefaults(defineProps<{
icon: VueConstructor
title: string
color: string
usedValue: number
usedTitle: string
usedInfo: string
availableTitle: string
actionTitle: string
onAction: () => void
isLoading: boolean
isDark?: boolean
useAction?: boolean
link?: string
}>(), {
isDark: false,
useAction: false,
link: '',
});
/**
* Returns progress bar styling which depends on provided prop values.
*/
const style = computed((): Record<string, string> => {
let color = '';
switch (true) {
case props.isDark:
color = '#091c45';
break;
case props.usedValue >= 80 && props.usedValue < 100:
color = '#ff8a00';
break;
case props.usedValue >= 100:
color = '#ff458b';
break;
default:
color = props.color;
}
return {
width: `${props.usedValue}%`,
'background-color': color,
};
});
</script>
<style scoped lang="scss">
.card {
font-family: 'font_regular', sans-serif;
padding: 24px;
background-color: var(--c-white);
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
border-radius: 10px;
&__header {
display: flex;
align-items: center;
margin-bottom: 16px;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 18px;
line-height: 27px;
color: var(--c-black);
margin-left: 8px;
}
}
&__track {
width: 100%;
height: 6px;
background: var(--c-grey-3);
border-radius: 100px;
position: relative;
margin-bottom: 16px;
&__fill {
max-width: 100%;
position: absolute;
border-radius: 100px;
top: 0;
bottom: 0;
left: 0;
}
}
&__data {
display: flex;
align-items: center;
justify-content: space-between;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 18px;
line-height: 27px;
color: var(--c-black);
}
&__info {
font-size: 14px;
line-height: 22px;
color: var(--c-grey-6);
}
&__action {
display: block;
font-size: 14px;
line-height: 22px;
color: var(--c-grey-6);
text-align: right;
text-decoration: underline !important;
cursor: pointer;
}
}
}
</style>

View File

@ -0,0 +1,328 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="limits-area">
<LimitCard
:icon="StorageIcon"
title="Storage"
color="#537cff"
:used-value="storageUsed"
:used-title="`${usedOrLimitFormatted(limits.storageUsed)} Used`"
:used-info="`Storage limit: ${usedOrLimitFormatted(limits.storageLimit, true)}`"
:available-title="`${availableFormatted(limits.storageLimit - limits.storageUsed)} Available`"
:action-title="usageActionTitle(storageUsed)"
:on-action="storageAction"
:is-loading="isLoading"
use-action
/>
<LimitCard
:icon="DownloadIcon"
title="Download"
color="#7b61ff"
:used-value="bandwidthUsed"
:used-title="`${usedOrLimitFormatted(limits.bandwidthUsed)} Used`"
:used-info="`Download limit: ${usedOrLimitFormatted(limits.bandwidthLimit, true)} per month`"
:available-title="`${availableFormatted(limits.bandwidthLimit - limits.bandwidthUsed)} Available`"
:action-title="usageActionTitle(bandwidthUsed)"
:on-action="bandwidthAction"
:is-loading="isLoading"
use-action
/>
<LimitCard
:icon="SegmentIcon"
title="Segments"
color="#003dc1"
:used-value="segmentUsed"
:used-title="`${limits.segmentUsed} Used`"
:used-info="`Segment limit: ${limits.segmentLimit}`"
:available-title="`${segmentsAvailable} Available`"
:action-title="usageActionTitle(segmentUsed, true)"
:on-action="startUpgradeFlow"
:is-loading="isLoading"
:use-action="!isPaidTier"
:link="segmentUsed < EIGHTY_PERCENT ?
'https://docs.storj.io/dcs/billing-payment-and-accounts-1/pricing/billing-and-payment#bcfOt' :
'https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212'"
/>
<LimitCard
v-if="coupon && isFreeTierCoupon"
:icon="CheckmarkIcon"
title="Free Tier"
color="#091c45"
:used-value="projectPricePercentage"
:used-title="`${projectPricePercentage.toFixed(0)}% Used`"
:used-info="`Free tier: ${centsToDollars(coupon?.amountOff)}`"
:available-title="`${remainingCouponAmount}% Available`"
:action-title="freeTierActionTitle"
:on-action="startUpgradeFlow"
:is-loading="isLoading"
:is-dark="isPaidTier"
:use-action="!isPaidTier"
link="https://docs.storj.io/dcs/billing-payment-and-accounts-1/pricing/free-tier"
/>
<LimitCard
v-if="coupon && !isFreeTierCoupon"
:icon="CheckmarkIcon"
title="Coupon"
color="#091c45"
:used-value="projectPricePercentage"
:used-title="`${projectPricePercentage.toFixed(0)}% Used`"
:used-info="`Coupon: ${centsToDollars(coupon?.amountOff)} monthly`"
:available-title="isPaidTier ?
`${centsToDollars(coupon?.amountOff)} per month` :
`${remainingCouponAmount}% Available`"
action-title="View coupons"
:on-action="navigateToCoupons"
:is-loading="isLoading"
is-dark
use-action
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { ProjectLimits } from '@/types/projects';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useBillingStore } from '@/store/modules/billingStore';
import { useAppStore } from '@/store/modules/appStore';
import { Size } from '@/utils/bytesSize';
import { Coupon, ProjectCharges } from '@/types/payments';
import { centsToDollars } from '@/utils/strings';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useRouter } from '@/utils/hooks';
import { RouteConfig } from '@/router';
import LimitCard from '@/components/project/dashboard/LimitCard.vue';
import StorageIcon from '@/../static/images/project/cloud.svg';
import DownloadIcon from '@/../static/images/project/download.svg';
import SegmentIcon from '@/../static/images/project/segment.svg';
import CheckmarkIcon from '@/../static/images/project/checkmark.svg';
const appStore = useAppStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const billingStore = useBillingStore();
const router = useRouter();
const props = defineProps<{
isLoading: boolean
}>();
const EIGHTY_PERCENT = 80;
const HUNDRED_PERCENT = 100;
/**
* Returns coupon from store.
*/
const coupon = computed((): Coupon | null => {
return billingStore.state.coupon;
});
/**
* Indicates if active coupon is free tier coupon.
*/
const isFreeTierCoupon = computed((): boolean => {
if (!coupon.value) {
return true;
}
const freeTierCouponName = 'Free Tier';
return coupon.value.name.includes(freeTierCouponName);
});
/**
* Indicates if user is in a paid tier status.
*/
const isPaidTier = computed((): boolean => {
return usersStore.state.user.paidTier;
});
/**
* Returns current project limits from store.
*/
const limits = computed((): ProjectLimits => {
return projectsStore.state.currentLimits;
});
/**
* Returns current project charges from store.
*/
const projectCharges = computed((): ProjectCharges => {
return billingStore.state.projectCharges as ProjectCharges;
});
/**
* Calculates storage usage percentage.
*/
const storageUsed = computed((): number => {
return (limits.value.storageUsed / limits.value.storageLimit) * HUNDRED_PERCENT;
});
/**
* Calculates bandwidth usage percentage.
*/
const bandwidthUsed = computed((): number => {
return (limits.value.bandwidthUsed / limits.value.bandwidthLimit) * HUNDRED_PERCENT;
});
/**
* Calculates segment usage percentage.
*/
const segmentUsed = computed((): number => {
return (limits.value.segmentUsed / limits.value.segmentLimit) * HUNDRED_PERCENT;
});
/**
* Calculates overall project price percentage depending on current coupon for current month.
*/
const projectPricePercentage = computed((): number => {
if (!coupon.value) {
return 0;
}
const selectedProjectID = projectsStore.state.selectedProject.id;
let projectPrice = projectCharges.value.getProjectPrice(selectedProjectID);
if (projectPrice > coupon.value.amountOff) {
projectPrice = coupon.value.amountOff;
}
return (projectPrice / coupon.value.amountOff) * HUNDRED_PERCENT;
});
/**
* Calculates remaining coupon amount percentage.
*/
const remainingCouponAmount = computed((): string => {
return (HUNDRED_PERCENT - projectPricePercentage.value).toFixed(0);
});
/**
* Returns free tier card CTA label.
*/
const freeTierActionTitle = computed((): string => {
switch (true) {
case !isPaidTier.value && projectPricePercentage.value >= EIGHTY_PERCENT && projectPricePercentage.value < HUNDRED_PERCENT:
return 'Upgrade';
case !isPaidTier.value && projectPricePercentage.value >= HUNDRED_PERCENT:
return 'Upgrade now';
default:
return 'Learn more';
}
});
/**
* Calculates remaining available segments amount.
*/
const segmentsAvailable = computed((): number => {
let available = limits.value.segmentLimit - limits.value.segmentUsed;
if (available < 0) {
available = 0;
}
return available;
});
/**
* Returns usage card CTA label.
*/
function usageActionTitle(usage: number, isSegment = false): string {
switch (true) {
case !isPaidTier.value && usage < EIGHTY_PERCENT:
return 'Need more?';
case !isPaidTier.value && usage >= EIGHTY_PERCENT && usage < HUNDRED_PERCENT:
return 'Upgrade';
case !isPaidTier.value && usage >= HUNDRED_PERCENT:
return 'Upgrade now';
case isPaidTier.value && usage < EIGHTY_PERCENT && !isSegment:
return 'Change limits';
case isPaidTier.value && usage < EIGHTY_PERCENT && isSegment:
return 'Learn more';
case isPaidTier.value && usage >= EIGHTY_PERCENT:
return 'Increase limits';
default:
return '';
}
}
/**
* Returns formatted value of available usage.
*/
function availableFormatted(diff: number): string {
const size = new Size(diff);
let value = size.formattedBytes;
if (parseFloat(value) < 0) {
value = '0';
}
return `${value} ${size.label}`;
}
/**
* Returns formatted value of used amount.
*/
function usedOrLimitFormatted(value: number, withoutSpace = false): string {
const size = new Size(value);
let formatted = `${size.formattedBytes} ${size.label}`;
if (withoutSpace) {
formatted = formatted.replace(' ', '');
}
return formatted;
}
/**
* Handles storage card CTA click.
*/
function storageAction(): void {
if (!isPaidTier.value) {
startUpgradeFlow();
return;
}
// toggle storage modal
}
/**
* Handles bandwidth card CTA click.
*/
function bandwidthAction(): void {
if (!isPaidTier.value) {
startUpgradeFlow();
return;
}
// toggle bandwidth modal
}
/**
* Starts upgrade account flow.
*/
function startUpgradeFlow(): void {
appStore.updateActiveModal(MODALS.upgradeAccount);
}
/**
* Navigates to billing coupons view.
*/
function navigateToCoupons(): void {
router.push(RouteConfig.Account.with(RouteConfig.Billing.with(RouteConfig.BillingCoupons)).path);
}
</script>
<style scoped lang="scss">
.limits-area {
display: grid;
grid-template-columns: calc(50% - 8px) calc(50% - 8px);
grid-gap: 16px;
margin-top: 16px;
}
</style>

View File

@ -103,6 +103,7 @@
</template>
</div>
</div>
<LimitsArea :is-loading="isDataFetching" />
<div class="project-dashboard__info">
<InfoContainer
title="Billing"
@ -183,6 +184,7 @@ import VInfo from '@/components/common/VInfo.vue';
import BucketsTable from '@/components/objects/BucketsTable.vue';
import EncryptionBanner from '@/components/objects/EncryptionBanner.vue';
import ProjectOwnershipTag from '@/components/project/ProjectOwnershipTag.vue';
import LimitsArea from '@/components/project/dashboard/LimitsArea.vue';
import NewProjectIcon from '@/../static/images/project/newProject.svg';
import InfoIcon from '@/../static/images/project/infoIcon.svg';
@ -428,8 +430,11 @@ onMounted(async (): Promise<void> => {
appStore.toggleHasJustLoggedIn();
}
await projectsStore.getDailyProjectData({ since: past, before: now });
await billingStore.getProjectUsageAndChargesCurrentRollup();
await Promise.all([
projectsStore.getDailyProjectData({ since: past, before: now }),
billingStore.getProjectUsageAndChargesCurrentRollup(),
billingStore.getCoupon(),
]);
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
} finally {

View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1.5C14.9706 1.5 19 5.52944 19 10.5C19 15.4706 14.9706 19.5 10 19.5C5.02944 19.5 1 15.4706 1 10.5C1 5.52944 5.02944 1.5 10 1.5ZM10 3.15C5.94071 3.15 2.65 6.44071 2.65 10.5C2.65 14.5593 5.94071 17.85 10 17.85C14.0593 17.85 17.35 14.5593 17.35 10.5C17.35 6.44071 14.0593 3.15 10 3.15ZM13.8388 8.00461C14.1531 8.31893 14.1608 8.82379 13.8618 9.14741L13.8388 9.17134L9.77957 13.2306C9.65841 13.3517 9.50962 13.429 9.35321 13.462C9.08427 13.5519 8.77588 13.4952 8.55428 13.2879L8.54466 13.2787L6.13418 10.8684C5.812 10.5462 5.812 10.0238 6.13418 9.70167C6.44851 9.38734 6.95336 9.37967 7.27698 9.67867L7.30091 9.70167L9.13808 11.5386L12.6721 8.00461C12.9943 7.68243 13.5166 7.68243 13.8388 8.00461Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 829 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.59239 16.4991L9.67232 16.5H16.52C18.4421 16.5 20 14.9408 20 13.0178C20 11.1188 18.4795 9.57069 16.5843 9.53614L16.5274 9.53559L15.8124 9.53567L15.7945 9.31148C15.5589 6.37966 13.1046 4.11194 10.1572 4.11194C7.84008 4.11194 5.76525 5.52367 4.9068 7.6636L4.88085 7.72953L4.81866 7.89045L4.30194 7.89037C1.92589 7.89037 0 9.81785 0 12.1952C0 14.546 1.88475 16.4614 4.23105 16.4994L4.29527 16.5L9.54038 16.4998L9.59239 16.4991ZM9.4971 14.8582H4.30193L4.25102 14.8578C2.80758 14.8343 1.64179 13.6496 1.64179 12.1952C1.64179 10.7243 2.83293 9.53216 4.30193 9.53216L5.94531 9.53219L6.41068 8.32539L6.4326 8.26973C7.03996 6.75576 8.51265 5.75373 10.1572 5.75373C12.2484 5.75373 13.9909 7.36374 14.158 9.44298L14.2973 11.1773L16.5199 11.1773L16.5619 11.1778C17.5547 11.1959 18.3582 12.014 18.3582 13.0178C18.3582 14.0344 17.5351 14.8582 16.52 14.8582L9.68142 14.8582L9.60902 14.8572L9.5763 14.8572L9.4971 14.8582Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5768 14.2261L15.5505 14.2535L10.5801 19.2238C10.2209 19.5831 9.64384 19.5918 9.27396 19.2501L9.24661 19.2238L4.27625 14.2535C3.90801 13.8852 3.90801 13.2882 4.27625 12.92C4.63551 12.5607 5.21254 12.5519 5.58242 12.8937L5.60976 12.92L8.9705 16.2804L8.97043 2.44293C8.97043 1.92217 9.3926 1.5 9.91337 1.5C10.4214 1.5 10.8356 1.90182 10.8556 2.40501L10.8563 2.44293L10.8564 16.2804L14.217 12.92C14.5762 12.5607 15.1533 12.5519 15.5231 12.8937L15.5505 12.92C15.9097 13.2792 15.9185 13.8562 15.5768 14.2261Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 639 B

View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.075 1.5L10.1294 1.50046L10.1837 1.50184C10.5902 1.50998 10.9901 1.54507 11.3816 1.6054C11.0975 2.13505 10.9273 2.73272 10.903 3.36705L10.9001 3.36552V17.6345C12.4045 16.8547 13.75 14.0385 13.75 10.5C13.75 9.33108 13.6032 8.24098 13.3523 7.27708C13.8844 7.51649 14.4763 7.65 15.1 7.65L15.1274 7.64969C15.3042 8.54541 15.4 9.50381 15.4 10.5C15.4 12.91 14.8396 15.0987 13.9269 16.7142C15.9841 15.4114 17.35 13.1152 17.35 10.5C17.35 9.37692 17.0981 8.31267 16.6477 7.36067C17.1747 7.15563 17.6493 6.84863 18.0465 6.46456C18.6566 7.67817 19 9.04898 19 10.5C19 15.3601 15.1477 19.3204 10.33 19.4941L10.1837 19.4982C10.1475 19.4994 10.1113 19.5 10.075 19.5H10C5.02944 19.5 1 15.4706 1 10.5C1 5.52944 5.02944 1.5 10 1.5H10.075ZM9.25004 3.36544C7.74556 4.14516 6.4 6.96138 6.4 10.5C6.4 14.0386 7.74556 16.8548 9.25004 17.6346V3.36544ZM6.30412 4.14542L6.2252 4.19209C4.08343 5.47653 2.65 7.82087 2.65 10.5C2.65 13.2119 4.11869 15.5807 6.30405 16.8545C5.34374 15.2267 4.75 12.9805 4.75 10.5C4.75 8.01948 5.34374 5.77335 6.30412 4.14542ZM15.175 1.5C16.3348 1.5 17.275 2.40662 17.275 3.525C17.275 4.64338 16.3348 5.55 15.175 5.55C14.0152 5.55 13.075 4.64338 13.075 3.525C13.075 2.40662 14.0152 1.5 15.175 1.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB