web/satellite: updated project dashboard data cards

Updated cards to show access grants count, buckets count, team size and billing status for current project

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

Change-Id: I7e8d3aa3e05548d593576c3a0633e9eb51f38cff
This commit is contained in:
Vitalii 2023-05-11 16:58:55 +03:00
parent 083b3d6fc1
commit 277a4c6aa3
3 changed files with 125 additions and 64 deletions

View File

@ -217,6 +217,8 @@ async function onProjectSelected(projectID: string): Promise<void> {
billingStore.getProjectUsageAndChargesCurrentRollup(),
projectsStore.getProjectLimits(projectID),
bucketsStore.getBuckets(FIRST_PAGE, projectID),
agStore.getAccessGrants(FIRST_PAGE, projectID),
pmStore.getProjectMembers(FIRST_PAGE, projectID),
]);
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.NAVIGATION_PROJECT_SELECTION);

View File

@ -3,7 +3,10 @@
<template>
<div class="info-container">
<h2 class="info-container__title">{{ title }}</h2>
<div class="info-container__header">
<component :is="icon" />
<h2 class="info-container__header__title">{{ title }}</h2>
</div>
<VLoader v-if="isDataFetching" height="40px" width="40px" />
<template v-else>
<p class="info-container__subtitle">{{ subtitle }}</p>
@ -14,9 +17,12 @@
</template>
<script setup lang="ts">
import { VueConstructor } from 'vue';
import VLoader from '@/components/common/VLoader.vue';
const props = withDefaults(defineProps<{
icon: VueConstructor,
isDataFetching: boolean,
title: string,
subtitle: string,
@ -34,30 +40,40 @@ const props = withDefaults(defineProps<{
padding: 24px;
width: calc(100% - 48px);
font-family: 'font_regular', sans-serif;
background-color: #fff;
background-color: var(--c-white);
box-shadow: 0 0 32px rgb(0 0 0 / 4%);
border-radius: 10px;
&__title {
font-family: 'font_medium', sans-serif;
font-size: 18px;
line-height: 27px;
color: #000;
&__header {
display: flex;
align-items: center;
:deep(path) {
fill: var(--c-black);
}
&__title {
font-family: 'font_medium', sans-serif;
font-size: 18px;
line-height: 27px;
color: var(--c-black);
margin-left: 8px;
}
}
&__subtitle {
font-size: 12px;
line-height: 18px;
color: #000;
color: var(--c-grey-6);
margin-bottom: 24px;
}
&__value {
font-family: 'font_bold', sans-serif;
font-size: 36px;
line-height: 47px;
font-size: 28px;
line-height: 36px;
letter-spacing: -0.02em;
color: #000;
color: var(--c-black);
}
}
</style>

View File

@ -106,42 +106,58 @@
<LimitsArea :is-loading="isDataFetching" />
<div class="project-dashboard__info">
<InfoContainer
title="Billing"
:subtitle="status"
:value="centsToDollars(estimatedCharges)"
:is-data-fetching="isDataFetching"
:icon="BucketsIcon"
title="Buckets"
:subtitle="`Last update ${now}`"
:value="bucketsCount.toString()"
:is-data-fetching="areBucketsFetching"
>
<template #side-value>
<p class="project-dashboard__info__label">Will be charged during next billing period</p>
<router-link :to="RouteConfig.Buckets.path" class="project-dashboard__info__link">
Go to buckets
</router-link>
</template>
</InfoContainer>
<InfoContainer
title="Objects"
:subtitle="`Updated ${now}`"
:value="limits.objectCount.toString()"
:icon="GrantsIcon"
title="Access Grants"
:subtitle="`Last update ${now}`"
:value="accessGrantsCount.toString()"
:is-data-fetching="isDataFetching"
>
<template #side-value>
<p class="project-dashboard__info__label" aria-roledescription="total-storage">
Total of {{ usedLimitFormatted(limits.storageUsed) }}
<router-link :to="RouteConfig.AccessGrants.path" class="project-dashboard__info__link">
Access management
</router-link>
</template>
</InfoContainer>
<InfoContainer
:icon="TeamIcon"
title="Users"
:subtitle="`Last update ${now}`"
:value="teamSize.toString()"
:is-data-fetching="isDataFetching"
>
<template #side-value>
<p class="project-dashboard__info__link" @click="onInviteUsersClick">
Invite project users
</p>
</template>
</InfoContainer>
<InfoContainer
title="Segments"
:subtitle="`Updated ${now}`"
:value="limits.segmentCount.toString()"
:icon="BillingIcon"
title="Billing"
:subtitle="status"
:value="isProAccount ? centsToDollars(estimatedCharges) : 'Free'"
:is-data-fetching="isDataFetching"
>
<template #side-value>
<a
<router-link
:to="RouteConfig.Account.with(RouteConfig.Billing.with(RouteConfig.BillingOverview)).path"
class="project-dashboard__info__link"
href="https://docs.storj.io/dcs/billing-payment-and-accounts-1/pricing#segments"
target="_blank"
rel="noopener noreferrer"
>
Learn more ->
</a>
Go to billing
</router-link>
</template>
</InfoContainer>
</div>
@ -171,6 +187,8 @@ import { useAppStore } from '@/store/modules/appStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { centsToDollars } from '@/utils/strings';
import VLoader from '@/components/common/VLoader.vue';
@ -188,6 +206,10 @@ import LimitsArea from '@/components/project/dashboard/LimitsArea.vue';
import NewProjectIcon from '@/../static/images/project/newProject.svg';
import InfoIcon from '@/../static/images/project/infoIcon.svg';
import BucketsIcon from '@/../static/images/navigation/buckets.svg';
import GrantsIcon from '@/../static/images/navigation/accessGrants.svg';
import TeamIcon from '@/../static/images/navigation/users.svg';
import BillingIcon from '@/../static/images/navigation/billing.svg';
const configStore = useConfigStore();
const bucketsStore = useBucketsStore();
@ -195,6 +217,8 @@ const appStore = useAppStore();
const billingStore = useBillingStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const pmStore = useProjectMembersStore();
const agStore = useAccessGrantsStore();
const notify = useNotify();
const router = useRouter();
@ -304,12 +328,33 @@ const bucketWasCreated = computed((): boolean => {
});
/**
* get selected project from store
* Get selected project from store.
*/
const selectedProject = computed((): Project => {
return projectsStore.state.selectedProject;
});
/**
* Returns current team size from store.
*/
const teamSize = computed((): number => {
return pmStore.state.page.totalCount;
});
/**
* Returns access grants count from store.
*/
const accessGrantsCount = computed((): number => {
return agStore.state.page.totalCount;
});
/**
* Returns access grants count from store.
*/
const bucketsCount = computed((): number => {
return bucketsStore.state.page.totalCount;
});
/**
* Hides server-side encryption banner.
*/
@ -332,6 +377,13 @@ function onUpgradeClick(): void {
appStore.updateActiveModal(MODALS.upgradeAccount);
}
/**
* Holds on invite users CTA click logic.
*/
function onInviteUsersClick(): void {
appStore.updateActiveModal(MODALS.addTeamMember);
}
/**
* Holds on create project button click logic.
*/
@ -407,6 +459,8 @@ onMounted(async (): Promise<void> => {
window.addEventListener('resize', recalculateChartWidth);
recalculateChartWidth();
const FIRST_PAGE = 1;
try {
const now = new Date();
const past = new Date();
@ -434,6 +488,8 @@ onMounted(async (): Promise<void> => {
projectsStore.getDailyProjectData({ since: past, before: now }),
billingStore.getProjectUsageAndChargesCurrentRollup(),
billingStore.getCoupon(),
pmStore.getProjectMembers(FIRST_PAGE, projectID),
agStore.getAccessGrants(FIRST_PAGE, projectID),
]);
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
@ -441,8 +497,6 @@ onMounted(async (): Promise<void> => {
isDataFetching.value = false;
}
const FIRST_PAGE = 1;
try {
await bucketsStore.getBuckets(FIRST_PAGE, projectID);
@ -497,7 +551,7 @@ onBeforeUnmount((): void => {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin: 63px -8px 14px;
margin: 44px -8px 14px;
> * {
margin: 2px 8px;
@ -640,24 +694,36 @@ onBeforeUnmount((): void => {
flex-wrap: wrap;
.info-container {
width: calc((100% - 32px) / 3);
width: calc((100% - 32px) / 4);
box-sizing: border-box;
}
&__label,
@media screen and (max-width: 1060px) {
> .info-container {
width: calc((100% - 16px) / 2);
margin-bottom: 16px;
}
}
@media screen and (max-width: 600px) {
> .info-container {
width: 100%;
}
}
&__link {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #000;
}
&__link {
color: var(--c-black);
cursor: pointer;
text-decoration: underline !important;
text-underline-position: under;
&:visited {
color: #000;
color: var(--c-black);
}
}
}
@ -734,20 +800,6 @@ onBeforeUnmount((): void => {
margin-bottom: 22px;
}
}
&__info {
margin-top: 52px;
> .info-container {
width: calc((100% - 25px) / 2);
margin-bottom: 24px;
}
> .info-container:last-child {
width: 100%;
margin-bottom: 0;
}
}
}
:deep(.range-selection__popup) {
@ -762,15 +814,6 @@ onBeforeUnmount((): void => {
&__charts__container:first-child {
margin-bottom: 20px;
}
&__info {
margin-top: 32px;
> .info-container {
width: 100%;
margin-bottom: 16px;
}
}
}
}
</style>