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:
parent
083b3d6fc1
commit
277a4c6aa3
@ -217,6 +217,8 @@ async function onProjectSelected(projectID: string): Promise<void> {
|
|||||||
billingStore.getProjectUsageAndChargesCurrentRollup(),
|
billingStore.getProjectUsageAndChargesCurrentRollup(),
|
||||||
projectsStore.getProjectLimits(projectID),
|
projectsStore.getProjectLimits(projectID),
|
||||||
bucketsStore.getBuckets(FIRST_PAGE, projectID),
|
bucketsStore.getBuckets(FIRST_PAGE, projectID),
|
||||||
|
agStore.getAccessGrants(FIRST_PAGE, projectID),
|
||||||
|
pmStore.getProjectMembers(FIRST_PAGE, projectID),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await notify.error(error.message, AnalyticsErrorEventSource.NAVIGATION_PROJECT_SELECTION);
|
await notify.error(error.message, AnalyticsErrorEventSource.NAVIGATION_PROJECT_SELECTION);
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="info-container">
|
<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" />
|
<VLoader v-if="isDataFetching" height="40px" width="40px" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p class="info-container__subtitle">{{ subtitle }}</p>
|
<p class="info-container__subtitle">{{ subtitle }}</p>
|
||||||
@ -14,9 +17,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { VueConstructor } from 'vue';
|
||||||
|
|
||||||
import VLoader from '@/components/common/VLoader.vue';
|
import VLoader from '@/components/common/VLoader.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
icon: VueConstructor,
|
||||||
isDataFetching: boolean,
|
isDataFetching: boolean,
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string,
|
subtitle: string,
|
||||||
@ -34,30 +40,40 @@ const props = withDefaults(defineProps<{
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
width: calc(100% - 48px);
|
width: calc(100% - 48px);
|
||||||
font-family: 'font_regular', sans-serif;
|
font-family: 'font_regular', sans-serif;
|
||||||
background-color: #fff;
|
background-color: var(--c-white);
|
||||||
box-shadow: 0 0 32px rgb(0 0 0 / 4%);
|
box-shadow: 0 0 32px rgb(0 0 0 / 4%);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
&__title {
|
&__header {
|
||||||
font-family: 'font_medium', sans-serif;
|
display: flex;
|
||||||
font-size: 18px;
|
align-items: center;
|
||||||
line-height: 27px;
|
|
||||||
color: #000;
|
: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 {
|
&__subtitle {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
color: #000;
|
color: var(--c-grey-6);
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__value {
|
&__value {
|
||||||
font-family: 'font_bold', sans-serif;
|
font-family: 'font_bold', sans-serif;
|
||||||
font-size: 36px;
|
font-size: 28px;
|
||||||
line-height: 47px;
|
line-height: 36px;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
color: #000;
|
color: var(--c-black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -106,42 +106,58 @@
|
|||||||
<LimitsArea :is-loading="isDataFetching" />
|
<LimitsArea :is-loading="isDataFetching" />
|
||||||
<div class="project-dashboard__info">
|
<div class="project-dashboard__info">
|
||||||
<InfoContainer
|
<InfoContainer
|
||||||
title="Billing"
|
:icon="BucketsIcon"
|
||||||
:subtitle="status"
|
title="Buckets"
|
||||||
:value="centsToDollars(estimatedCharges)"
|
:subtitle="`Last update ${now}`"
|
||||||
:is-data-fetching="isDataFetching"
|
:value="bucketsCount.toString()"
|
||||||
|
:is-data-fetching="areBucketsFetching"
|
||||||
>
|
>
|
||||||
<template #side-value>
|
<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>
|
</template>
|
||||||
</InfoContainer>
|
</InfoContainer>
|
||||||
<InfoContainer
|
<InfoContainer
|
||||||
title="Objects"
|
:icon="GrantsIcon"
|
||||||
:subtitle="`Updated ${now}`"
|
title="Access Grants"
|
||||||
:value="limits.objectCount.toString()"
|
:subtitle="`Last update ${now}`"
|
||||||
|
:value="accessGrantsCount.toString()"
|
||||||
:is-data-fetching="isDataFetching"
|
:is-data-fetching="isDataFetching"
|
||||||
>
|
>
|
||||||
<template #side-value>
|
<template #side-value>
|
||||||
<p class="project-dashboard__info__label" aria-roledescription="total-storage">
|
<router-link :to="RouteConfig.AccessGrants.path" class="project-dashboard__info__link">
|
||||||
Total of {{ usedLimitFormatted(limits.storageUsed) }}
|
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>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</InfoContainer>
|
</InfoContainer>
|
||||||
<InfoContainer
|
<InfoContainer
|
||||||
title="Segments"
|
:icon="BillingIcon"
|
||||||
:subtitle="`Updated ${now}`"
|
title="Billing"
|
||||||
:value="limits.segmentCount.toString()"
|
:subtitle="status"
|
||||||
|
:value="isProAccount ? centsToDollars(estimatedCharges) : 'Free'"
|
||||||
:is-data-fetching="isDataFetching"
|
:is-data-fetching="isDataFetching"
|
||||||
>
|
>
|
||||||
<template #side-value>
|
<template #side-value>
|
||||||
<a
|
<router-link
|
||||||
|
:to="RouteConfig.Account.with(RouteConfig.Billing.with(RouteConfig.BillingOverview)).path"
|
||||||
class="project-dashboard__info__link"
|
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 ->
|
Go to billing →
|
||||||
</a>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</InfoContainer>
|
</InfoContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -171,6 +187,8 @@ import { useAppStore } from '@/store/modules/appStore';
|
|||||||
import { useBucketsStore } from '@/store/modules/bucketsStore';
|
import { useBucketsStore } from '@/store/modules/bucketsStore';
|
||||||
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 { useProjectMembersStore } from '@/store/modules/projectMembersStore';
|
||||||
|
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
|
||||||
import { centsToDollars } from '@/utils/strings';
|
import { centsToDollars } from '@/utils/strings';
|
||||||
|
|
||||||
import VLoader from '@/components/common/VLoader.vue';
|
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 NewProjectIcon from '@/../static/images/project/newProject.svg';
|
||||||
import InfoIcon from '@/../static/images/project/infoIcon.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 configStore = useConfigStore();
|
||||||
const bucketsStore = useBucketsStore();
|
const bucketsStore = useBucketsStore();
|
||||||
@ -195,6 +217,8 @@ const appStore = useAppStore();
|
|||||||
const billingStore = useBillingStore();
|
const billingStore = useBillingStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
|
const pmStore = useProjectMembersStore();
|
||||||
|
const agStore = useAccessGrantsStore();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const router = useRouter();
|
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 => {
|
const selectedProject = computed((): Project => {
|
||||||
return projectsStore.state.selectedProject;
|
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.
|
* Hides server-side encryption banner.
|
||||||
*/
|
*/
|
||||||
@ -332,6 +377,13 @@ function onUpgradeClick(): void {
|
|||||||
appStore.updateActiveModal(MODALS.upgradeAccount);
|
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.
|
* Holds on create project button click logic.
|
||||||
*/
|
*/
|
||||||
@ -407,6 +459,8 @@ onMounted(async (): Promise<void> => {
|
|||||||
window.addEventListener('resize', recalculateChartWidth);
|
window.addEventListener('resize', recalculateChartWidth);
|
||||||
recalculateChartWidth();
|
recalculateChartWidth();
|
||||||
|
|
||||||
|
const FIRST_PAGE = 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const past = new Date();
|
const past = new Date();
|
||||||
@ -434,6 +488,8 @@ onMounted(async (): Promise<void> => {
|
|||||||
projectsStore.getDailyProjectData({ since: past, before: now }),
|
projectsStore.getDailyProjectData({ since: past, before: now }),
|
||||||
billingStore.getProjectUsageAndChargesCurrentRollup(),
|
billingStore.getProjectUsageAndChargesCurrentRollup(),
|
||||||
billingStore.getCoupon(),
|
billingStore.getCoupon(),
|
||||||
|
pmStore.getProjectMembers(FIRST_PAGE, projectID),
|
||||||
|
agStore.getAccessGrants(FIRST_PAGE, projectID),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await notify.error(error.message, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
|
await notify.error(error.message, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
|
||||||
@ -441,8 +497,6 @@ onMounted(async (): Promise<void> => {
|
|||||||
isDataFetching.value = false;
|
isDataFetching.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIRST_PAGE = 1;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await bucketsStore.getBuckets(FIRST_PAGE, projectID);
|
await bucketsStore.getBuckets(FIRST_PAGE, projectID);
|
||||||
|
|
||||||
@ -497,7 +551,7 @@ onBeforeUnmount((): void => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 63px -8px 14px;
|
margin: 44px -8px 14px;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin: 2px 8px;
|
margin: 2px 8px;
|
||||||
@ -640,24 +694,36 @@ onBeforeUnmount((): void => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.info-container {
|
.info-container {
|
||||||
width: calc((100% - 32px) / 3);
|
width: calc((100% - 32px) / 4);
|
||||||
box-sizing: border-box;
|
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 {
|
&__link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: #000;
|
color: var(--c-black);
|
||||||
}
|
cursor: pointer;
|
||||||
|
|
||||||
&__link {
|
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
text-underline-position: under;
|
text-underline-position: under;
|
||||||
|
|
||||||
&:visited {
|
&:visited {
|
||||||
color: #000;
|
color: var(--c-black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -734,20 +800,6 @@ onBeforeUnmount((): void => {
|
|||||||
margin-bottom: 22px;
|
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) {
|
:deep(.range-selection__popup) {
|
||||||
@ -762,15 +814,6 @@ onBeforeUnmount((): void => {
|
|||||||
&__charts__container:first-child {
|
&__charts__container:first-child {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
|
||||||
margin-top: 32px;
|
|
||||||
|
|
||||||
> .info-container {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user