web/satellite: properly style all projects dashboard

This change matches the all projects dashboard styling to the figma as
closely as currently possible.

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

Change-Id: I93edd898fbcee954265737052a1967d318370924
This commit is contained in:
Wilfred Asomani 2023-06-07 09:42:26 +00:00
parent 8cdc5bd107
commit 3d1e429156
7 changed files with 389 additions and 137 deletions

View File

@ -6,7 +6,7 @@
<div class="load" /> <div class="load" />
<LoaderImage class="loading-icon" /> <LoaderImage class="loading-icon" />
</div> </div>
<div v-else ref="dashboardContent" class="all-dashboard"> <div v-else class="all-dashboard">
<div class="all-dashboard__bars"> <div class="all-dashboard__bars">
<BetaSatBar v-if="isBetaSatellite" /> <BetaSatBar v-if="isBetaSatellite" />
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" /> <MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
@ -17,64 +17,6 @@
<div class="all-dashboard__content"> <div class="all-dashboard__content">
<div class="all-dashboard__content__divider" /> <div class="all-dashboard__content__divider" />
<div class="all-dashboard__banners">
<UpgradeNotification
v-if="isPaidTierBannerShown"
class="all-dashboard__banners__upgrade"
:open-add-p-m-modal="togglePMModal"
/>
<v-banner
v-if="isAccountFrozen && !isLoading && dashboardContent"
class="all-dashboard__banners__freeze"
severity="critical"
:dashboard-ref="dashboardContent"
>
<template #text>
<p class="medium">Your account was frozen due to billing issues. Please update your payment information.</p>
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
</template>
</v-banner>
<v-banner
v-if="isAccountWarned && !isLoading && dashboardContent"
class="all-dashboard__banners__warning"
severity="warning"
:dashboard-ref="dashboardContent"
>
<template #text>
<p class="medium">Your account will be frozen soon due to billing issues. Please update your payment information.</p>
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
</template>
</v-banner>
<v-banner
v-if="limitState.hundredIsShown && !isLoading && dashboardContent"
class="all-dashboard__banners__hundred-limit"
severity="critical"
:on-click="() => setIsHundredLimitModalShown(true)"
:dashboard-ref="dashboardContent"
>
<template #text>
<p class="medium">{{ limitState.hundredLabel }}</p>
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
</template>
</v-banner>
<v-banner
v-if="limitState.eightyIsShown && !isLoading && dashboardContent"
class="all-dashboard__banners__eighty-limit"
severity="warning"
:on-click="() => setIsEightyLimitModalShown(true)"
:dashboard-ref="dashboardContent"
>
<template #text>
<p class="medium">{{ limitState.eightyLabel }}</p>
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
</template>
</v-banner>
</div>
<router-view /> <router-view />
<limit-warning-modal <limit-warning-modal
@ -142,8 +84,6 @@ import BetaSatBar from '@/components/infoBars/BetaSatBar.vue';
import MFARecoveryCodeBar from '@/components/infoBars/MFARecoveryCodeBar.vue'; import MFARecoveryCodeBar from '@/components/infoBars/MFARecoveryCodeBar.vue';
import AllModals from '@/components/modals/AllModals.vue'; import AllModals from '@/components/modals/AllModals.vue';
import LimitWarningModal from '@/components/modals/LimitWarningModal.vue'; import LimitWarningModal from '@/components/modals/LimitWarningModal.vue';
import VBanner from '@/components/common/VBanner.vue';
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
import LoaderImage from '@/../static/images/common/loadIcon.svg'; import LoaderImage from '@/../static/images/common/loadIcon.svg';
@ -181,7 +121,6 @@ const isSessionActive = ref<boolean>(false);
const isSessionRefreshing = ref<boolean>(false); const isSessionRefreshing = ref<boolean>(false);
const isHundredLimitModalShown = ref<boolean>(false); const isHundredLimitModalShown = ref<boolean>(false);
const isEightyLimitModalShown = ref<boolean>(false); const isEightyLimitModalShown = ref<boolean>(false);
const dashboardContent = ref<HTMLElement | null>(null);
/** /**
* Returns the session duration from the store. * Returns the session duration from the store.
@ -217,14 +156,7 @@ const isAccountFrozen = computed((): boolean => {
}); });
/** /**
* Indicates if account was warned due to billing issues. * Returns all needed information for limit modal when bandwidth or storage close to limits.
*/
const isAccountWarned = computed((): boolean => {
return usersStore.state.user.freezeStatus.warned;
});
/**
* Returns all needed information for limit banner and modal when bandwidth or storage close to limits.
*/ */
type LimitedState = { type LimitedState = {
eightyIsShown: boolean; eightyIsShown: boolean;
@ -297,13 +229,6 @@ const limitState = computed((): LimitedState => {
return result; return result;
}); });
/**
* Whether the current route is the billing page.
*/
const isBillingPage = computed(() => {
return route.path.includes(RouteConfig.Billing2.path);
});
/** /**
* Indicates if satellite is in beta. * Indicates if satellite is in beta.
*/ */
@ -326,31 +251,6 @@ const showMFARecoveryCodeBar = computed((): boolean => {
return user.isMFAEnabled && user.mfaRecoveryCodeCount < recoveryCodeWarningThreshold; return user.isMFAEnabled && user.mfaRecoveryCodeCount < recoveryCodeWarningThreshold;
}); });
/**
* Returns whether the user has reached project limits.
*/
const hasReachedProjectLimit = computed((): boolean => {
const projectLimit: number = usersStore.state.user.projectLimit;
const projectsCount: number = projectsStore.projectsCount(usersStore.state.user.id);
return projectsCount === projectLimit;
});
/* whether the paid tier banner should be shown */
const isPaidTierBannerShown = computed((): boolean => {
return !usersStore.state.user.paidTier
&& !isBillingPage.value
&& joinedWhileAgo.value;
});
/* whether the user joined more than 7 days ago */
const joinedWhileAgo = computed((): boolean => {
const createdAt = usersStore.state.user.createdAt as Date | null;
if (!createdAt) return true; // true so we can show the banner regardless
const millisPerDay = 24 * 60 * 60 * 1000;
return ((Date.now() - createdAt.getTime()) / millisPerDay) > 7;
});
function setIsEightyLimitModalShown(value: boolean): void { function setIsEightyLimitModalShown(value: boolean): void {
isEightyLimitModalShown.value = value; isEightyLimitModalShown.value = value;
} }
@ -359,13 +259,6 @@ function setIsHundredLimitModalShown(value: boolean): void {
isHundredLimitModalShown.value = value; isHundredLimitModalShown.value = value;
} }
/**
* Redirects to Billing Page.
*/
async function redirectToBillingPage(): Promise<void> {
await router.push(RouteConfig.AccountSettings.with(RouteConfig.Billing2.with(RouteConfig.BillingPaymentMethods2)).path);
}
/** /**
* Redirects to log in screen. * Redirects to log in screen.
*/ */
@ -714,19 +607,6 @@ onBeforeUnmount(() => {
} }
} }
} }
&__banners {
margin-bottom: 20px;
&__upgrade,
&__project-limit,
&__freeze,
&__warning,
&__hundred-limit,
&__eighty-limit {
margin: 20px 0 0;
}
}
} }
.load { .load {

View File

@ -0,0 +1,234 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="all-dashboard-banners">
<UpgradeNotification
v-if="isPaidTierBannerShown"
class="all-dashboard-banners__upgrade"
:open-add-p-m-modal="togglePMModal"
/>
<v-banner
v-if="isAccountFrozen && parentRef"
class="all-dashboard-banners__freeze"
severity="critical"
:dashboard-ref="parentRef"
>
<template #text>
<p class="medium">Your account was frozen due to billing issues. Please update your payment information.</p>
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
</template>
</v-banner>
<v-banner
v-if="isAccountWarned && parentRef"
class="all-dashboard-banners__warning"
severity="warning"
:dashboard-ref="parentRef"
>
<template #text>
<p class="medium">Your account will be frozen soon due to billing issues. Please update your payment information.</p>
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
</template>
</v-banner>
<v-banner
v-if="limitState.hundredIsShown && parentRef"
class="all-dashboard-banners__hundred-limit"
severity="critical"
:on-click="() => setIsHundredLimitModalShown(true)"
:dashboard-ref="parentRef"
>
<template #text>
<p class="medium">{{ limitState.hundredLabel }}</p>
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
</template>
</v-banner>
<v-banner
v-if="limitState.eightyIsShown && parentRef"
class="all-dashboard-banners__eighty-limit"
severity="warning"
:on-click="() => setIsEightyLimitModalShown(true)"
:dashboard-ref="parentRef"
>
<template #text>
<p class="medium">{{ limitState.eightyLabel }}</p>
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
</template>
</v-banner>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUsersStore } from '@/store/modules/usersStore';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { RouteConfig } from '@/types/router';
import VBanner from '@/components/common/VBanner.vue';
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
const router = useRouter();
const usersStore = useUsersStore();
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const props = defineProps<{
parentRef: HTMLElement;
}>();
const isHundredLimitModalShown = ref<boolean>(false);
const isEightyLimitModalShown = ref<boolean>(false);
/**
* Returns all needed information for limit banner and modal when bandwidth or storage close to limits.
*/
type LimitedState = {
eightyIsShown: boolean;
hundredIsShown: boolean;
eightyLabel: string;
eightyModalLimitType: string;
eightyModalTitle: string;
hundredLabel: string;
hundredModalTitle: string;
hundredModalLimitType: string;
}
const limitState = computed((): LimitedState => {
const result: LimitedState = {
eightyIsShown: false,
hundredIsShown: false,
eightyLabel: '',
eightyModalLimitType: '',
eightyModalTitle: '',
hundredLabel: '',
hundredModalTitle: '',
hundredModalLimitType: '',
};
if (usersStore.state.user.paidTier || isAccountFrozen.value) {
return result;
}
const currentLimits = projectsStore.state.currentLimits;
const limitTypeArr = [
{ name: 'egress', usedPercent: Math.round(currentLimits.bandwidthUsed * 100 / currentLimits.bandwidthLimit) },
{ name: 'storage', usedPercent: Math.round(currentLimits.storageUsed * 100 / currentLimits.storageLimit) },
{ name: 'segment', usedPercent: Math.round(currentLimits.segmentUsed * 100 / currentLimits.segmentLimit) },
];
const hundredPercent: string[] = [];
const eightyPercent: string[] = [];
limitTypeArr.forEach((limitType) => {
if (limitType.usedPercent >= 80) {
if (limitType.usedPercent >= 100) {
hundredPercent.push(limitType.name);
} else {
eightyPercent.push(limitType.name);
}
}
});
if (eightyPercent.length !== 0) {
result.eightyIsShown = true;
const eightyPercentString = eightyPercent.join(' and ');
result.eightyLabel = `You've used 80% of your ${eightyPercentString} limit. Avoid interrupting your usage by upgrading your account.`;
result.eightyModalTitle = `80% ${eightyPercentString} limit used`;
result.eightyModalLimitType = eightyPercentString;
}
if (hundredPercent.length !== 0) {
result.hundredIsShown = true;
const hundredPercentString = hundredPercent.join(' and ');
result.hundredLabel = `URGENT: Youve reached the ${hundredPercentString} limit for your project. Upgrade to avoid any service interruptions.`;
result.hundredModalTitle = `URGENT: Youve reached the ${hundredPercentString} limit for your project.`;
result.hundredModalLimitType = hundredPercentString;
}
return result;
});
/**
* Indicates if account was frozen due to billing issues.
*/
const isAccountFrozen = computed((): boolean => {
return usersStore.state.user.freezeStatus.frozen;
});
/**
* Indicates if account was warned due to billing issues.
*/
const isAccountWarned = computed((): boolean => {
return usersStore.state.user.freezeStatus.warned;
});
/* whether the paid tier banner should be shown */
const isPaidTierBannerShown = computed((): boolean => {
return !usersStore.state.user.paidTier
&& joinedWhileAgo.value;
});
/* whether the user joined more than 7 days ago */
const joinedWhileAgo = computed((): boolean => {
const createdAt = usersStore.state.user.createdAt as Date | null;
if (!createdAt) return true; // true so we can show the banner regardless
const millisPerDay = 24 * 60 * 60 * 1000;
return ((Date.now() - createdAt.getTime()) / millisPerDay) > 7;
});
/**
* Opens add payment method modal.
*/
function togglePMModal(): void {
isHundredLimitModalShown.value = false;
isEightyLimitModalShown.value = false;
if (!usersStore.state.user.paidTier) {
appStore.updateActiveModal(MODALS.upgradeAccount);
}
}
function setIsEightyLimitModalShown(value: boolean): void {
isEightyLimitModalShown.value = value;
}
function setIsHundredLimitModalShown(value: boolean): void {
isHundredLimitModalShown.value = value;
}
/**
* Redirects to Billing Page.
*/
async function redirectToBillingPage(): Promise<void> {
await router.push(RouteConfig.AccountSettings.with(RouteConfig.Billing2.with(RouteConfig.BillingPaymentMethods2)).path);
}
</script>
<style scoped lang="scss">
.all-dashboard-banners {
margin-bottom: 20px;
&__upgrade,
&__project-limit,
&__freeze,
&__warning,
&__hundred-limit,
&__eighty-limit {
margin: 20px 0 0;
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
}
}
</style>

View File

@ -9,6 +9,8 @@
<VButton <VButton
class="header__content__actions__docs" class="header__content__actions__docs"
icon="resources" icon="resources"
border-radius="8px"
font-size="12px"
is-white is-white
:link="link" :link="link"
:on-press="sendDocsEvent" :on-press="sendDocsEvent"
@ -285,7 +287,19 @@ function sendDocsEvent(): void {
&__docs { &__docs {
padding: 10px 16px; padding: 10px 16px;
border-radius: 8px; box-shadow: 0 0 20px rgb(0 0 0 / 4%);
:deep(.label) {
& > svg {
height: 14px;
width: 14px;
}
color: var(--c-black) !important;
font-weight: 700;
line-height: 20px;
}
} }
} }
} }

View File

@ -189,14 +189,15 @@ async function onLogout(): Promise<void> {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 14px 18px; padding: 10px 16px;
box-sizing: border-box; box-sizing: border-box;
color: var(--c-grey-6);
cursor: pointer; cursor: pointer;
background: var(--c-white); background: var(--c-white);
border: 1px solid var(--c-grey-3); border: 1px solid var(--c-grey-3);
border-radius: 8px; border-radius: 8px;
height: 44px; height: 44px;
color: var(--c-black);
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
&:hover, &:hover,
&:active, &:active,
@ -213,13 +214,20 @@ async function onLogout(): Promise<void> {
&__icon { &__icon {
transition-duration: 0.5s; transition-duration: 0.5s;
margin-right: 10px; margin-right: 10px;
height: 16px;
width: 16px;
:deep(path) {
fill: var(--c-black);
}
} }
&__label { &__label {
font-family: 'font_medium', sans-serif; font-family: 'font_medium', sans-serif;
font-size: 16px; line-height: 20px;
line-height: 23px; font-weight: 700;
color: var(--c-grey-6); font-size: 12px;
color: var(--c-black);
margin-right: 10px; margin-right: 10px;
white-space: nowrap; white-space: nowrap;
} }

View File

@ -2,13 +2,30 @@
// See LICENSE for copying information. // See LICENSE for copying information.
<template> <template>
<div class="my-projects"> <div ref="content" class="my-projects">
<div class="my-projects__header"> <div class="my-projects__header">
<span class="my-projects__header__title">My Projects</span> <div class="my-projects__header__title">
<span>My Projects</span>
<span class="my-projects__header__title__views">
<span
class="my-projects__header__title__views__icon"
@click="() => onViewChangeClicked('table')"
>
<table-icon />
</span>
<span
class="my-projects__header__title__views__icon"
@click="() => onViewChangeClicked('cards')"
>
<cards-icon />
</span>
</span>
</div>
<span class="my-projects__header__right"> <span class="my-projects__header__right">
<span class="my-projects__header__right__text">View</span> <span class="my-projects__header__right__text">View</span>
<v-chip <v-chip
class="my-projects__header__right__table-chip"
label="Table" label="Table"
:is-selected="isTableViewSelected" :is-selected="isTableViewSelected"
:icon="TableIcon" :icon="TableIcon"
@ -16,21 +33,37 @@
/> />
<v-chip <v-chip
class="my-projects__header__right__card-chip"
label="Cards" label="Cards"
:is-selected="!isTableViewSelected" :is-selected="!isTableViewSelected"
:icon="CardsIcon" :icon="CardsIcon"
@select="() => onViewChangeClicked('cards')" @select="() => onViewChangeClicked('cards')"
/> />
<VButton
class="my-projects__header__right__mobile-button"
icon="addcircle"
border-radius="8px"
font-size="12px"
is-white
:on-press="onCreateProjectClicked"
label="Create New Project"
/>
<VButton <VButton
class="my-projects__header__right__button" class="my-projects__header__right__button"
icon="addcircle" icon="addcircle"
border-radius="8px"
font-size="12px"
is-white is-white
:on-press="onCreateProjectClicked" :on-press="onCreateProjectClicked"
label="Create a Project" label="Create a Project"
/> />
</span> </span>
</div> </div>
<all-projects-dashboard-banners v-if="content" :parent-ref="content" />
<div v-if="projects.length || invites.length" class="my-projects__list"> <div v-if="projects.length || invites.length" class="my-projects__list">
<projects-table v-if="isTableViewSelected" :invites="invites" class="my-projects__list__table" /> <projects-table v-if="isTableViewSelected" :invites="invites" class="my-projects__list__table" />
<div v-else-if="!isTableViewSelected" class="my-projects__list__cards"> <div v-else-if="!isTableViewSelected" class="my-projects__list__cards">
@ -64,6 +97,7 @@ import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore'; import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore'; import { useConfigStore } from '@/store/modules/configStore';
import ProjectsTable from '@/views/all-dashboard/components/ProjectsTable.vue'; import ProjectsTable from '@/views/all-dashboard/components/ProjectsTable.vue';
import AllProjectsDashboardBanners from '@/views/all-dashboard/components/AllProjectsDashboardBanners.vue';
import VButton from '@/components/common/VButton.vue'; import VButton from '@/components/common/VButton.vue';
import VChip from '@/components/common/VChip.vue'; import VChip from '@/components/common/VChip.vue';
@ -79,6 +113,8 @@ const projectsStore = useProjectsStore();
const analytics = new AnalyticsHttpApi(); const analytics = new AnalyticsHttpApi();
const content = ref<HTMLElement | null>(null);
const hasProjectTableViewConfigured = ref(appStore.hasProjectTableViewConfigured()); const hasProjectTableViewConfigured = ref(appStore.hasProjectTableViewConfigured());
/** /**
@ -138,20 +174,51 @@ function onCreateProjectClicked(): void {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@media screen and (width <= 425px) { @media screen and (width <= 500px) {
margin-top: 20px;
flex-direction: column; flex-direction: column;
align-items: start; align-items: start;
gap: 20px; gap: 20px;
&__button { &__title {
width: 100% !important; display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
&__views {
display: flex !important;
align-items: center;
justify-content: flex-end;
column-gap: 5px;
&__icon {
height: 24px;
width: 24px;
:deep(path),
:deep(rect){
fill: var(--c-black);
}
}
}
} }
} }
&__title { &__title {
font-family: 'font_bold', sans-serif; font-family: 'font_bold', sans-serif;
font-weight: 700;
font-size: 24px; font-size: 24px;
line-height: 31px; line-height: 31px;
@media screen and (width <= 500px) {
font-size: 18px;
line-height: 27px;
}
&__views {
display: none;
}
} }
&__right { &__right {
@ -161,15 +228,42 @@ function onCreateProjectClicked(): void {
justify-content: flex-end; justify-content: flex-end;
column-gap: 12px; column-gap: 12px;
@media screen and (width <= 500px) {
width: 100%;
&__text,
&__button,
&__table-chip,
&__card-chip {
display: none;
}
&__mobile-button {
display: flex !important;
}
}
&__text { &__text {
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: var(--c-grey-6); color: var(--c-grey-6);
} }
&__button { &__button,
&__mobile-button {
padding: 10px 16px; padding: 10px 16px;
border-radius: 8px; box-shadow: 0 0 20px rgb(0 0 0 / 4%);
:deep(.label) {
color: var(--c-black) !important;
font-weight: 700;
line-height: 20px;
}
}
&__mobile-button {
display: none;
} }
} }
} }

View File

@ -18,6 +18,15 @@
label="Join Project" label="Join Project"
class="invitation-item__menu__button" class="invitation-item__menu__button"
/> />
<v-button
:loading="isLoading"
:disabled="isLoading"
:on-press="onJoinClicked"
border-radius="8px"
font-size="12px"
label="Join"
class="invitation-item__menu__mobile-button"
/>
<div class="invitation-item__menu"> <div class="invitation-item__menu">
<div class="invitation-item__menu__icon" @click.stop="toggleDropDown"> <div class="invitation-item__menu__icon" @click.stop="toggleDropDown">
<div class="invitation-item__menu__icon__content" :class="{open: isDropdownOpen}"> <div class="invitation-item__menu__icon__content" :class="{open: isDropdownOpen}">
@ -80,7 +89,7 @@ const itemToRender = computed((): { [key: string]: unknown | string[] } => {
}; };
} }
return { info: [ props.invitation.projectName, `Created ${props.invitation.invitedDate}` ] }; return { info: [ props.invitation.projectName, props.invitation.projectDescription ] };
}); });
/** /**
@ -149,6 +158,19 @@ function closeDropDown() {
&__button { &__button {
padding: 10px 16px; padding: 10px 16px;
@media screen and (width <= 500px) {
display: none;
}
}
&__mobile-button {
display: none;
padding: 10px 16px;
@media screen and (width <= 500px) {
display: flex;
}
} }
&__icon { &__icon {

View File

@ -84,7 +84,7 @@ const itemToRender = computed((): { [key: string]: unknown | string[] } => {
}; };
} }
return { info: [ props.project.name, `Created ${props.project.createdDate()}` ] }; return { info: [ props.project.name, props.project.description ] };
}); });
/** /**