web/satellite: added loaders across all the UI. Removed most of the requests from initial load

Added loader spinners across all of the UI to be visible while data is being fetched.
Removed most of the requests from the initial load of the satellite dashboard.
Removed useless requests after creating of new projects.
This should make user's experience much more better since load time of the app is much lower than it was before.

Change-Id: Ib0941ad4eee6b3caf781d132062b55cb17703fe7
This commit is contained in:
Vitalii Shpital 2021-06-09 18:13:19 +03:00
parent c27da95742
commit ed28fa3ff9
44 changed files with 763 additions and 671 deletions

View File

@ -20,10 +20,12 @@
width="203px"
height="44px"
:on-press="onCreateClick"
:is-disabled="areGrantsFetching"
/>
</div>
</div>
<div v-if="accessGrantsList.length" class="access-grants-items">
<VLoader v-if="areGrantsFetching" width="100px" height="100px" class="grants-loader"/>
<div v-if="accessGrantsList.length && !areGrantsFetching" class="access-grants-items">
<SortAccessGrantsHeader :on-header-click-callback="onHeaderSectionClickCallback"/>
<div class="access-grants-items__content">
<VList
@ -40,7 +42,7 @@
:on-page-click-callback="onPageClick"
/>
</div>
<EmptyState v-else />
<EmptyState v-if="!accessGrantsList.length && !areGrantsFetching" />
<ConfirmDeletePopup
v-if="isDeleteClicked"
@close="onClearSelection"
@ -59,6 +61,7 @@ import EmptyState from '@/components/accessGrants/EmptyState.vue';
import SortAccessGrantsHeader from '@/components/accessGrants/SortingHeader.vue';
import VButton from '@/components/common/VButton.vue';
import VList from '@/components/common/VList.vue';
import VLoader from '@/components/common/VLoader.vue';
import VPagination from '@/components/common/VPagination.vue';
import { RouteConfig } from '@/router';
@ -88,6 +91,7 @@ declare interface ResetPagination {
VPagination,
VButton,
ConfirmDeletePopup,
VLoader,
},
})
export default class AccessGrants extends Vue {
@ -98,6 +102,8 @@ export default class AccessGrants extends Vue {
*/
private isDeleteClicked: boolean = false;
public areGrantsFetching: boolean = true;
public $refs!: {
pagination: HTMLElement & ResetPagination;
};
@ -107,7 +113,9 @@ export default class AccessGrants extends Vue {
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(FETCH, 1);
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
this.areGrantsFetching = false;
} catch (error) {
await this.$notify.error(`Unable to fetch Access Grants. ${error.message}`);
}
@ -262,4 +270,8 @@ export default class AccessGrants extends Vue {
}
}
}
.grants-loader {
margin-top: 50px;
}
</style>

View File

@ -34,7 +34,8 @@
</div>
<div class="permissions__content__right__buckets-select">
<p class="permissions__content__right__buckets-select__label">Buckets</p>
<BucketsSelection />
<VLoader v-if="areBucketNamesFetching" width="50px" height="50px"/>
<BucketsSelection v-else/>
</div>
<div class="permissions__content__right__bucket-bullets">
<div
@ -53,11 +54,11 @@
width="100%"
height="48px"
:on-press="onContinueInBrowserClick"
:is-disabled="isLoading || !isAccessGrantsWebWorkerReady"
:is-disabled="isLoading || !isAccessGrantsWebWorkerReady || areBucketNamesFetching"
/>
<p
class="permissions__cli-link"
:class="{ disabled: !isAccessGrantsWebWorkerReady || isLoading }"
:class="{ disabled: !isAccessGrantsWebWorkerReady || isLoading || areBucketNamesFetching }"
@click.stop="onContinueInCLIClick"
>
Continue in CLI
@ -72,6 +73,7 @@ import BucketNameBullet from '@/components/accessGrants/permissions/BucketNameBu
import BucketsSelection from '@/components/accessGrants/permissions/BucketsSelection.vue';
import DurationSelection from '@/components/accessGrants/permissions/DurationSelection.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import BackIcon from '@/../static/images/accessGrants/back.svg';
@ -87,6 +89,7 @@ import { DurationPermission } from '@/types/accessGrants';
BucketNameBullet,
DurationSelection,
VButton,
VLoader,
},
})
export default class PermissionsStep extends Vue {
@ -99,6 +102,7 @@ export default class PermissionsStep extends Vue {
public isUpload: boolean = true;
public isList: boolean = true;
public isDelete: boolean = true;
public areBucketNamesFetching: boolean = true;
/**
* Lifecycle hook after initial render.
@ -124,6 +128,8 @@ export default class PermissionsStep extends Vue {
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH_ALL_BUCKET_NAMES);
this.areBucketNamesFetching = false;
} catch (error) {
await this.$notify.error(`Unable to fetch all bucket names. ${error.message}`);
}

View File

@ -3,6 +3,7 @@
<template>
<div class="account-billing-area">
<InfoBar/>
<div class="account-billing-area__notification-container" v-if="hasNoCreditCard">
<div class="account-billing-area__notification-container__negative-balance" v-if="isBalanceNegative">
<NegativeBalanceIcon/>
@ -21,16 +22,20 @@
<div class="account-billing-area__title-area" v-if="userHasOwnProject" :class="{ 'custom-position': hasNoCreditCard && (isBalanceLow || isBalanceNegative) }">
<div class="account-billing-area__title-area__balance-area">
<div class="account-billing-area__title-area__balance-area__free-credits">
<span class="account-billing-area__title-area__balance-area__free-credits__amount">
Free Credits: {{ balance.freeCredits | centsToDollars }}
</span>
<p class="account-billing-area__title-area__balance-area__free-credits__label">Free Credits:</p>
<VLoader v-if="isBalanceFetching" width="20px" height="20px"/>
<p v-else>{{ balance.freeCredits | centsToDollars }}</p>
</div>
<div @click.stop="toggleBalanceDropdown" class="account-billing-area__title-area__balance-area__tokens-area">
<span class="account-billing-area__title-area__balance-area__tokens-area__amount" :style="{ color: balanceColor }">
Available Balance: {{ balance.coins | centsToDollars }}
</span>
<HideIcon v-if="isBalanceDropdownShown"/>
<ExpandIcon v-else/>
<p class="account-billing-area__title-area__balance-area__tokens-area__label" :style="{ color: balanceColor }">
Available Balance:
</p>
<VLoader v-if="isBalanceFetching" width="20px" height="20px"/>
<p v-else>
{{ balance.coins | centsToDollars }}
</p>
<HideIcon v-if="isBalanceDropdownShown" class="icon"/>
<ExpandIcon v-else class="icon"/>
<HistoryDropdown
v-show="isBalanceDropdownShown"
@close="closeDropdown"
@ -57,7 +62,9 @@ import SmallDepositHistory from '@/components/account/billing/depositAndBillingH
import EstimatedCostsAndCredits from '@/components/account/billing/estimatedCostsAndCredits/EstimatedCostsAndCredits.vue';
import CreditsHistory from '@/components/account/billing/freeCredits/CreditsHistory.vue';
import HistoryDropdown from '@/components/account/billing/HistoryDropdown.vue';
import InfoBar from '@/components/account/billing/InfoBar.vue';
import PaymentMethods from '@/components/account/billing/paymentMethods/PaymentMethods.vue';
import VLoader from '@/components/common/VLoader.vue';
import DatePickerIcon from '@/../static/images/account/billing/datePicker.svg';
import ExpandIcon from '@/../static/images/account/billing/expand.svg';
@ -67,7 +74,6 @@ import NegativeBalanceIcon from '@/../static/images/account/billing/negativeBala
import { RouteConfig } from '@/router';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { AccountBalance } from '@/types/payments';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
@ -84,29 +90,23 @@ import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
ExpandIcon,
HideIcon,
CreditsHistory,
InfoBar,
VLoader,
},
})
export default class BillingArea extends Vue {
public readonly creditHistoryRoute: string = RouteConfig.Account.with(RouteConfig.CreditsHistory).path;
public readonly balanceHistoryRoute: string = RouteConfig.Account.with(RouteConfig.DepositHistory).path;
public isBalanceFetching: boolean = true;
/**
* Mounted lifecycle hook before initial render.
* Fetches billing history and project limits.
* Mounted lifecycle hook after initial render.
* Fetches account balance.
*/
public async beforeMount(): Promise<void> {
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PAYMENTS_HISTORY);
if (this.$store.getters.canUserCreateFirstProject && !this.userHasOwnProject) {
await this.$store.dispatch(APP_STATE_ACTIONS.SHOW_CREATE_PROJECT_BUTTON);
}
} catch (error) {
await this.$notify.error(error.message);
}
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_BALANCE);
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
this.isBalanceFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
@ -203,7 +203,6 @@ export default class BillingArea extends Vue {
</script>
<style scoped lang="scss">
.label-header {
display: none;
}
@ -239,9 +238,8 @@ export default class BillingArea extends Vue {
font-weight: bold;
font-size: 16px;
line-height: 148.31%;
margin: 30px 0;
margin: 30px 0 10px 0;
display: inline-block;
margin-bottom: 10px;
}
&__input-wrapper {
@ -290,7 +288,7 @@ export default class BillingArea extends Vue {
display: flex;
align-items: center;
justify-content: space-between;
margin: 60px 0 20px 0;
margin: 20px 0;
&__balance-area {
display: flex;
@ -303,13 +301,13 @@ export default class BillingArea extends Vue {
align-items: center;
position: relative;
cursor: pointer;
margin-right: 50px;
color: #768394;
font-size: 16px;
line-height: 19px;
&__amount {
&__label {
margin-right: 10px;
font-size: 16px;
line-height: 19px;
white-space: nowrap;
}
}
@ -320,11 +318,12 @@ export default class BillingArea extends Vue {
cursor: default;
margin-right: 50px;
color: #768394;
font-size: 16px;
line-height: 19px;
&__amount {
&__label {
margin-right: 10px;
font-size: 16px;
line-height: 19px;
white-space: nowrap;
}
}
}
@ -340,7 +339,6 @@ export default class BillingArea extends Vue {
justify-content: flex-start;
padding: 20px 20px 20px 20px;
border-radius: 12px;
margin-bottom: 32px;
&__text {
font-family: 'font_medium', sans-serif;
@ -364,4 +362,9 @@ export default class BillingArea extends Vue {
.custom-position {
margin: 30px 0 20px 0;
}
.icon {
min-width: 14px;
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,174 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="billing-info-bar">
<div class="billing-info-bar__info">
<VLoader class="billing-info-loader" v-if="isDataFetching" width="20px" height="20px"/>
<b v-else class="billing-info-bar__info__storage-value">{{ storageRemaining }}</b>
<span class="billing-info-bar__info__storage-label">of Storage Remaining</span>
<VLoader class="billing-info-loader" v-if="isDataFetching" width="20px" height="20px"/>
<b v-else class="billing-info-bar__info__bandwidth-value">{{ bandwidthRemaining }}</b>
<span class="billing-info-bar__info__bandwidth-label">of Bandwidth Remaining</span>
<router-link class="billing-info-bar__info__button" :to="projectDashboardPath">Details</router-link>
</div>
<a
class="billing-info-bar__link"
:href="projectLimitsIncreaseRequestURL"
target="_blank"
rel="noopener noreferrer"
>
Request Limit Increase ->
</a>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VLoader from '@/components/common/VLoader.vue';
import { RouteConfig } from '@/router';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { Size } from '@/utils/bytesSize';
import { MetaUtils } from '@/utils/meta';
/**
* VBanner is common banner for needed pages
*/
@Component({
components: {
VLoader,
},
})
export default class InfoBar extends Vue {
public readonly projectDashboardPath: string = RouteConfig.ProjectDashboard.path;
public isDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches project limits.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
this.isDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Returns formatted string of remaining storage.
*/
public get storageRemaining(): string {
const storageUsed = this.$store.state.projectsModule.currentLimits.storageUsed;
const storageLimit = this.$store.state.projectsModule.currentLimits.storageLimit;
const difference = storageLimit - storageUsed;
if (difference < 0) {
return '0 Bytes';
}
const remaining = new Size(difference, 2);
return `${remaining.formattedBytes}${remaining.label}`;
}
/**
* Returns formatted string of remaining bandwidth.
*/
public get bandwidthRemaining(): string {
const bandwidthUsed = this.$store.state.projectsModule.currentLimits.bandwidthUsed;
const bandwidthLimit = this.$store.state.projectsModule.currentLimits.bandwidthLimit;
const difference = bandwidthLimit - bandwidthUsed;
if (difference < 0) {
return '0 Bytes';
}
const remaining = new Size(difference, 2);
return `${remaining.formattedBytes}${remaining.label}`;
}
/**
* Returns project limits increase request url from config.
*/
public get projectLimitsIncreaseRequestURL(): string {
return MetaUtils.getMetaContent('project-limits-increase-request-url');
}
}
</script>
<style scoped lang="scss">
.billing-info-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #dce5f2;
width: calc(100% - 60px);
padding: 10px 30px;
font-family: 'font_regular', sans-serif;
color: #2b3543;
&__info {
display: flex;
align-items: center;
&__storage-value,
&__storage-label,
&__bandwidth-value,
&__bandwidth-label {
margin-right: 5px;
font-size: 14px;
line-height: 17px;
white-space: nowrap;
}
&__button {
padding: 5px 10px;
margin-left: 15px;
background-color: #fff;
opacity: 0.8;
border-radius: 6px;
font-size: 12px;
line-height: 15px;
letter-spacing: 0.02em;
color: rgba(43, 53, 67, 0.5);
}
}
&__link {
font-size: 14px;
line-height: 17px;
font-family: 'font_medium', sans-serif;
color: #2683ff;
}
}
.billing-info-loader {
margin-right: 5px;
}
@media screen and (max-width: 768px) {
.billing-info-bar {
&__info {
&__storage-value,
&__storage-label,
&__bandwidth-value,
&__bandwidth-label {
white-space: unset;
}
}
}
}
</style>

View File

@ -9,15 +9,18 @@
</div>
<h1 class="history-area__title" v-if="isBillingHistory">Billing History</h1>
<h1 class="history-area__title" v-else>Balance History</h1>
<div class="history-area__content" v-if="historyItems.length > 0">
<SortingHeader/>
<PaymentsItem
v-for="item in historyItems"
:billing-item="item"
:key="item.id"
/>
</div>
<h2 class="history-area__empty-state" v-else>No Items Yet</h2>
<VLoader v-if="isDataFetching" height="100px" width="100px" class="history-loader"/>
<template v-else>
<div class="history-area__content" v-if="historyItems.length > 0">
<SortingHeader/>
<PaymentsItem
v-for="item in historyItems"
:billing-item="item"
:key="item.id"
/>
</div>
<h2 class="history-area__empty-state" v-else>No Items Yet</h2>
</template>
</div>
</template>
@ -26,10 +29,12 @@ import { Component, Vue } from 'vue-property-decorator';
import PaymentsItem from '@/components/account/billing/depositAndBillingHistory/PaymentsItem.vue';
import SortingHeader from '@/components/account/billing/depositAndBillingHistory/SortingHeader.vue';
import VLoader from '@/components/common/VLoader.vue';
import BackImage from '@/../static/images/account/billing/back.svg';
import { RouteConfig } from '@/router';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
@Component({
@ -37,9 +42,26 @@ import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
PaymentsItem,
SortingHeader,
BackImage,
VLoader,
},
})
export default class DetailedHistory extends Vue {
public isDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches payments history.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PAYMENTS_HISTORY);
this.isDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Returns list of history items depending on route name.
*/
@ -134,6 +156,10 @@ export default class DetailedHistory extends Vue {
}
}
.history-loader {
margin-top: 50px;
}
::-webkit-scrollbar,
::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb {

View File

@ -3,21 +3,24 @@
<template>
<div class="current-month-area">
<h1 class="current-month-area__costs">{{ priceSummary | centsToDollars }}</h1>
<span class="current-month-area__title">Estimated Charges for {{ chosenPeriod }}</span>
<p class="current-month-area__info">
If you still have Storage and Bandwidth remaining in your free tier, you wont be charged. This information
is to help you estimate what charges would have been had you graduated to the paid tier.
</p>
<div class="current-month-area__content">
<p class="current-month-area__content__title">DETAILS</p>
<UsageAndChargesItem
v-for="usageAndCharges in projectUsageAndCharges"
:item="usageAndCharges"
:key="usageAndCharges.projectId"
class="item"
/>
</div>
<VLoader class="consts-loader" v-if="isDataFetching"/>
<template v-else>
<h1 class="current-month-area__costs">{{ priceSummary | centsToDollars }}</h1>
<span class="current-month-area__title">Estimated Charges for {{ chosenPeriod }}</span>
<p class="current-month-area__info">
If you still have Storage and Bandwidth remaining in your free tier, you wont be charged. This information
is to help you estimate what charges would have been had you graduated to the paid tier.
</p>
<div class="current-month-area__content">
<p class="current-month-area__content__title">DETAILS</p>
<UsageAndChargesItem
v-for="usageAndCharges in projectUsageAndCharges"
:item="usageAndCharges"
:key="usageAndCharges.projectId"
class="item"
/>
</div>
</template>
</div>
</template>
@ -26,8 +29,10 @@ import { Component, Vue } from 'vue-property-decorator';
import UsageAndChargesItem from '@/components/account/billing/estimatedCostsAndCredits/UsageAndChargesItem.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { ProjectUsageAndCharges } from '@/types/payments';
import { MONTHS_NAMES } from '@/utils/constants/date';
@ -35,17 +40,22 @@ import { MONTHS_NAMES } from '@/utils/constants/date';
components: {
VButton,
UsageAndChargesItem,
VLoader,
},
})
export default class EstimatedCostsAndCredits extends Vue {
public isDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches current project usage rollup.
* Fetches projects and usage rollup.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_BALANCE);
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
this.isDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
@ -136,4 +146,8 @@ export default class EstimatedCostsAndCredits extends Vue {
.item {
border-top: 1px solid #c7cdd2;
}
.consts-loader {
padding-bottom: 40px;
}
</style>

View File

@ -16,12 +16,15 @@
<div class="credit-history__title-area">
<h1 class="credit-history__title">Credit History</h1>
</div>
<SortingHeader/>
<CreditsItem
v-for="(item, index) in historyItems"
:key="index"
:credits-item="item"
/>
<VLoader v-if="isHistoryFetching"/>
<template v-else>
<SortingHeader/>
<CreditsItem
v-for="(item, index) in historyItems"
:key="index"
:credits-item="item"
/>
</template>
</div>
</div>
</div>
@ -30,12 +33,13 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import AddCouponCode from '@/components/account/billing/freeCredits/AddCouponCode.vue';
import CreditsItem from '@/components/account/billing/freeCredits/CreditsItem.vue';
import SortingHeader from '@/components/account/billing/freeCredits/SortingHeader.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import { RouteConfig } from '@/router';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
@Component({
@ -43,9 +47,25 @@ import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
CreditsItem,
SortingHeader,
VButton,
VLoader,
},
})
export default class CreditsHistory extends Vue {
public isHistoryFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches payments history.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PAYMENTS_HISTORY);
this.isHistoryFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Returns list of free credit history items.
@ -78,7 +98,6 @@ export default class CreditsHistory extends Vue {
public get couponCodeUIEnabled(): boolean {
return this.$store.state.appStateModule.couponCodeUIEnabled;
}
}
</script>

View File

@ -78,7 +78,6 @@ export default class AddCardForm extends Vue {
setTimeout(() => {
if (!this.userHasOwnProject) {
this.$router.push(RouteConfig.CreateProject.path);
this.$store.dispatch(APP_STATE_ACTIONS.SHOW_CREATE_PROJECT_BUTTON);
}
}, 500);
}, 2000);

View File

@ -71,7 +71,8 @@
Your card is secured by Stripe through TLS and AES-256 encryption. Your information is secure.
</span>
</div>
<div class="payment-methods-area__existing-cards-container" v-if="!noCreditCards">
<VLoader v-if="areCardsFetching" class="pm-loader"/>
<div class="payment-methods-area__existing-cards-container" v-if="!noCreditCards && !areCardsFetching">
<CardComponent
v-for="card in creditCards"
:key="card.id"
@ -90,6 +91,7 @@ import AddStorjForm from '@/components/account/billing/paymentMethods/AddStorjFo
import CardComponent from '@/components/account/billing/paymentMethods/CardComponent.vue';
import PaymentsBonus from '@/components/account/billing/paymentMethods/PaymentsBonus.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import LockImage from '@/../static/images/account/billing/lock.svg';
import SuccessImage from '@/../static/images/account/billing/success.svg';
@ -98,11 +100,6 @@ import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { CreditCard } from '@/types/payments';
import { PaymentMethodsBlockState } from '@/utils/constants/billingEnums';
const {
GET_CREDIT_CARDS,
GET_BALANCE,
} = PAYMENTS_ACTIONS;
interface AddCardConfirm {
onConfirmAddStripe(): Promise<void>;
}
@ -116,6 +113,7 @@ interface AddCardConfirm {
PaymentsBonus,
LockImage,
SuccessImage,
VLoader,
},
})
export default class PaymentMethods extends Vue {
@ -125,15 +123,18 @@ export default class PaymentMethods extends Vue {
public isLoaded: boolean = false;
public isAddCardClicked: boolean = false;
public isAddStorjClicked: boolean = false;
public areCardsFetching: boolean = true;
/**
* Lifecycle hook after initial render where credit cards are fetched.
*/
public mounted() {
public async mounted(): Promise<void> {
try {
this.$store.dispatch(GET_CREDIT_CARDS);
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_CREDIT_CARDS);
this.areCardsFetching = false;
} catch (error) {
this.$notify.error(error.message);
await this.$notify.error(error.message);
}
}
@ -307,7 +308,7 @@ export default class PaymentMethods extends Vue {
}
.button-moved {
top: 95px;
top: 83px;
}
.payment-methods-area {
@ -427,4 +428,8 @@ export default class PaymentMethods extends Vue {
.reduced {
height: 170px;
}
.pm-loader {
margin-top: 40px;
}
</style>

View File

@ -1,130 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="info-bar" :class="{ blue: isBlue }">
<div class="info-bar__info-area">
<b class="info-bar__info-area__first-value">{{ firstValue }}</b>
<span class="info-bar__info-area__first-description">{{ firstDescription }}</span>
<b class="info-bar__info-area__second-value">{{ secondValue }}</b>
<span class="info-bar__info-area__second-description">{{ secondDescription }}</span>
<router-link v-if="path && !isProjectDashboard" class="info-bar__info-area__button" :to="path">Details</router-link>
</div>
<a
class="info-bar__link"
:class="{ blue: isBlue }"
:href="link"
target="_blank"
rel="noopener noreferrer"
>
{{ linkLabel }}
</a>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteConfig } from '@/router';
/**
* VBanner is custom banner on top of all pages in Dashboard
*/
@Component
export default class VInfoBar extends Vue {
@Prop({default: ''})
private readonly firstValue: string;
@Prop({default: ''})
private readonly secondValue: string;
@Prop({default: ''})
private readonly firstDescription: string;
@Prop({default: ''})
private readonly secondDescription: string;
@Prop({default: ''})
private readonly link: string;
@Prop({default: ''})
private readonly linkLabel: string;
@Prop({default: ''})
private readonly path: string;
@Prop({default: false})
private readonly isBlue: boolean;
/**
* Indicates if current route is project dashboard.
*/
public get isProjectDashboard(): boolean {
return this.$route.name === RouteConfig.ProjectDashboard.name;
}
}
</script>
<style scoped lang="scss">
.info-bar {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #dce5f2;
width: calc(100% - 60px);
padding: 10px 30px;
font-family: 'font_regular', sans-serif;
color: #2b3543;
&__info-area {
display: flex;
align-items: center;
&__first-value,
&__second-value,
&__first-description,
&__second-description {
margin-right: 5px;
font-size: 14px;
line-height: 17px;
white-space: nowrap;
}
&__button {
padding: 5px 10px;
margin-left: 15px;
background-color: #fff;
opacity: 0.8;
border-radius: 6px;
font-size: 12px;
line-height: 15px;
letter-spacing: 0.02em;
color: rgba(43, 53, 67, 0.5);
}
}
&__link {
font-size: 14px;
line-height: 17px;
font-family: 'font_medium', sans-serif;
color: #2683ff;
}
}
.blue {
background-color: #2582ff;
color: #fff;
}
@media screen and (max-width: 768px) {
.info-bar {
&__info-area {
&__first-value,
&__second-value,
&__first-description,
&__second-description {
white-space: unset;
}
}
}
}
</style>

View File

@ -2,18 +2,28 @@
// See LICENSE for copying information.
<template>
<div :style="style" class="loader"/>
<div class="loader">
<LoaderImage :class="{ white: isWhite }" :style="style"/>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
import LoaderImage from '@/../static/images/common/loader.svg';
@Component({
components: {
LoaderImage,
},
})
export default class VLoader extends Vue {
@Prop({ default: 'inherit' })
@Prop({ default: 'unset' })
private readonly width: string;
@Prop({ default: 'inherit' })
@Prop({ default: 'unset' })
private readonly height: string;
@Prop({ default: false })
private readonly isWhite: boolean;
/**
* Returns loader's width and height from props.
@ -26,14 +36,15 @@ export default class VLoader extends Vue {
<style scoped lang="scss">
.loader {
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
width: 100%;
display: flex;
justify-content: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
.white {
path {
fill: #fff;
}
}
</style>

View File

@ -43,20 +43,6 @@ import ProjectDropdown from './ProjectDropdown.vue';
export default class ProjectSelection extends Vue {
private isLoading: boolean = false;
/**
* Life cycle hook before initial render.
* Toggles new project button visibility depending on user reaching project count limit or having payment method.
*/
public beforeMount(): void {
if (this.isProjectLimitReached || !this.$store.getters.canUserCreateFirstProject) {
this.$store.dispatch(APP_STATE_ACTIONS.HIDE_CREATE_PROJECT_BUTTON);
return;
}
this.$store.dispatch(APP_STATE_ACTIONS.SHOW_CREATE_PROJECT_BUTTON);
}
/**
* Indicates if current route is onboarding tour.
*/
@ -108,13 +94,6 @@ export default class ProjectSelection extends Vue {
this.$store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
}
/**
* Indicates if project count limit is reached.
*/
private get isProjectLimitReached(): boolean {
return this.$store.getters.projectsCount >= this.$store.getters.user.projectLimit;
}
}
</script>

View File

@ -11,8 +11,8 @@
</div>
</div>
<VLoader
width="120px"
height="120px"
width="100px"
height="100px"
class="buckets-view__loader"
v-if="isLoading"
/>

View File

@ -65,15 +65,8 @@ import HeaderedInput from '@/components/common/HeaderedInput.vue';
import VButton from '@/components/common/VButton.vue';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { ProjectFields } from '@/types/projects';
import {
APP_STATE_ACTIONS,
PM_ACTIONS,
} from '@/utils/constants/actionNames';
import { LocalData } from '@/utils/localData';
@Component({
@ -152,18 +145,6 @@ export default class NewProjectPopup extends Vue {
this.selectCreatedProject();
try {
await this.fetchProjectMembers();
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.createdProjectId);
} catch (error) {
await this.$notify.error(`Unable to create project. ${error.message}`);
}
this.clearAccessGrants();
this.clearBucketUsage();
await this.$notify.success('Project created successfully!');
this.isLoading = false;
@ -177,33 +158,6 @@ export default class NewProjectPopup extends Vue {
private selectCreatedProject(): void {
this.$store.dispatch(PROJECTS_ACTIONS.SELECT, this.createdProjectId);
LocalData.setSelectedProjectId(this.createdProjectId);
if (this.$store.getters.projectsCount >= this.$store.getters.user.projectLimit) {
this.$store.dispatch(APP_STATE_ACTIONS.HIDE_CREATE_PROJECT_BUTTON);
}
}
/**
* Clears project members store and fetches new.
*/
private async fetchProjectMembers(): Promise<void> {
await this.$store.dispatch(PM_ACTIONS.CLEAR);
const fistPage = 1;
await this.$store.dispatch(PM_ACTIONS.FETCH, fistPage);
}
/**
* Clears access grants store.
*/
private clearAccessGrants(): void {
this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CLEAR);
}
/**
* Clears bucket usage store.
*/
private clearBucketUsage(): void {
this.$store.dispatch(BUCKET_ACTIONS.CLEAR);
}
}
</script>
@ -211,6 +165,7 @@ export default class NewProjectPopup extends Vue {
<style scoped lang="scss">
.full-input {
width: 100%;
margin-top: 20px;
}
.create-project-area {

View File

@ -10,20 +10,28 @@
</p>
</div>
<ProjectUsage/>
<ProjectSummary/>
<BucketArea/>
<ProjectSummary :is-data-fetching="isSummaryDataFetching"/>
<div v-if="areBucketsFetching" class="dashboard-area__container">
<p class="dashboard-area__container__title">Buckets</p>
<VLoader/>
</div>
<BucketArea v-else/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VLoader from '@/components/common/VLoader.vue';
import BucketArea from '@/components/project/buckets/BucketArea.vue';
import ProjectSummary from '@/components/project/summary/ProjectSummary.vue';
import ProjectUsage from '@/components/project/usage/ProjectUsage.vue';
import { RouteConfig } from '@/router';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
import { MetaUtils } from '@/utils/meta';
@Component({
@ -31,22 +39,38 @@ import { MetaUtils } from '@/utils/meta';
BucketArea,
ProjectUsage,
ProjectSummary,
VLoader,
},
})
export default class ProjectDashboard extends Vue {
public areBucketsFetching: boolean = true;
public isSummaryDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches buckets, usage rollup, project members and access grants.
*/
public mounted(): void {
public async mounted(): Promise<void> {
if (!this.$store.getters.selectedProject.id) {
this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
await this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
return;
}
const projectLimit: number = this.$store.getters.user.projectLimit;
if (projectLimit && this.$store.getters.projectsCount < projectLimit) {
this.$store.dispatch(APP_STATE_ACTIONS.SHOW_CREATE_PROJECT_BUTTON);
const FIRST_PAGE = 1;
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, FIRST_PAGE);
this.areBucketsFetching = false;
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
await this.$store.dispatch(PM_ACTIONS.FETCH, FIRST_PAGE);
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.FETCH, FIRST_PAGE);
this.isSummaryDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
@ -68,7 +92,7 @@ export default class ProjectDashboard extends Vue {
<style scoped lang="scss">
.dashboard-area {
padding: 50px 30px 60px 30px;
padding: 30px 30px 60px 30px;
font-family: 'font_regular', sans-serif;
&__header-wrapper {
@ -91,5 +115,20 @@ export default class ProjectDashboard extends Vue {
margin: 10px 0 0 0;
}
}
&__container {
background-color: #fff;
border-radius: 6px;
padding: 20px;
margin-top: 30px;
&__title {
margin: 0 0 20px 0;
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 16px;
color: #1b2533;
}
}
}
</style>

View File

@ -65,21 +65,6 @@ const {
},
})
export default class BucketArea extends Vue {
/**
* Lifecycle hook after initial render where buckets list is fetched.
*/
public async mounted(): Promise<void> {
if (!this.$store.getters.selectedProject.id) {
return;
}
try {
await this.$store.dispatch(FETCH, 1);
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Lifecycle hook before component destruction where buckets search query is cleared.
*/
@ -178,7 +163,7 @@ export default class BucketArea extends Vue {
<style scoped lang="scss">
.buckets-area {
margin-top: 36px;
margin-top: 30px;
position: relative;
&__pagination-area {
@ -198,9 +183,9 @@ export default class BucketArea extends Vue {
&__title {
margin: 0;
font-family: 'font_bold', sans-serif;
font-size: 22px;
line-height: 27px;
color: #384b65;
font-size: 16px;
line-height: 16px;
color: #1b2533;
}
}

View File

@ -4,7 +4,8 @@
<template>
<section class="project-summary">
<h1 class="project-summary__title">Details</h1>
<div class="project-summary__items">
<VLoader v-if="isDataFetching"/>
<div v-else class="project-summary__items">
<SummaryItem
class="right-indent"
background-color="#f5f6fa"
@ -42,35 +43,20 @@
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Component, Prop, Vue } from 'vue-property-decorator';
import VLoader from '@/components/common/VLoader.vue';
import SummaryItem from '@/components/project/summary/SummaryItem.vue';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
@Component({
components: {
SummaryItem,
VLoader,
},
})
export default class ProjectSummary extends Vue {
/**
* Lifecycle hook after initial render.
* Fetches buckets and project usage and charges for current rollup.
*/
public async mounted(): Promise<void> {
if (!this.$store.getters.selectedProject.id) return;
const FIRST_PAGE = 1;
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, FIRST_PAGE);
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
} catch (error) {
await this.$notify.error(error.message);
}
}
@Prop({ default: true })
public readonly isDataFetching: boolean;
/**
* teamSize returns project members amount for selected project.
@ -105,8 +91,8 @@ export default class ProjectSummary extends Vue {
<style scoped lang="scss">
.project-summary {
margin-top: 30px;
padding: 25px;
width: calc(100% - 50px);
padding: 20px;
width: calc(100% - 40px);
background-color: #fff;
border-radius: 6px;

View File

@ -8,12 +8,14 @@
title="Storage"
:used="storageUsed"
:limit="storageLimit"
:is-data-fetching="isDataFetching"
/>
<UsageArea
class="project-usage__bandwidth-used"
title="Bandwidth"
:used="bandwidthUsed"
:limit="bandwidthLimit"
:is-data-fetching="isDataFetching"
/>
</div>
</template>
@ -31,6 +33,8 @@ import { PROJECTS_ACTIONS } from '@/store/modules/projects';
},
})
export default class ProjectUsage extends Vue {
public isDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches project limits.
@ -42,6 +46,8 @@ export default class ProjectUsage extends Vue {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
this.isDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}

View File

@ -4,16 +4,19 @@
<template>
<div class="usage-area">
<p class="usage-area__title">{{title}}</p>
<pre class="usage-area__remaining">{{remainingFormatted}} Remaining</pre>
<VBar
:current="used"
:max="limit"
color="#0068DC"
/>
<div class="usage-area__limits-area">
<pre class="usage-area__limits-area__title">{{title}} Used</pre>
<pre class="usage-area__limits-area__limits">{{usedFormatted}} / {{limitFormatted}}</pre>
</div>
<VLoader v-if="isDataFetching"/>
<template v-else>
<pre class="usage-area__remaining">{{remainingFormatted}} Remaining</pre>
<VBar
:current="used"
:max="limit"
color="#0068DC"
/>
<div class="usage-area__limits-area">
<pre class="usage-area__limits-area__title">{{title}} Used</pre>
<pre class="usage-area__limits-area__limits">{{usedFormatted}} / {{limitFormatted}}</pre>
</div>
</template>
</div>
</template>
@ -21,12 +24,14 @@
import { Component, Prop, Vue } from 'vue-property-decorator';
import VBar from '@/components/common/VBar.vue';
import VLoader from '@/components/common/VLoader.vue';
import { Dimensions, Size } from '@/utils/bytesSize';
@Component({
components: {
VBar,
VLoader,
},
})
export default class UsageArea extends Vue {
@ -36,6 +41,8 @@ export default class UsageArea extends Vue {
public readonly used: number;
@Prop({default: 0})
public readonly limit: number;
@Prop({default: true})
public readonly isDataFetching: boolean;
/**
* Returns formatted remaining amount.
@ -86,7 +93,7 @@ export default class UsageArea extends Vue {
}
.usage-area {
padding: 35px;
padding: 20px;
border-radius: 6px;
background-color: #fff;

View File

@ -0,0 +1,131 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="projects-info-bar">
<div class="projects-info-bar__info">
<p class="projects-info-bar__info__message">
You have used
<VLoader v-if="isDataFetching" class="pr-info-loader" is-white="true" width="15px" height="15px"/>
<span class="projects-info-bar__info__message__value" v-else>{{ projectsCount }}</span>
of your
<VLoader v-if="isDataFetching" class="pr-info-loader" is-white="true" width="15px" height="15px"/>
<span class="projects-info-bar__info__message__value" v-else>{{ projectLimit }}</span>
available projects.
</p>
</div>
<a
class="projects-info-bar__link"
:href="projectLimitsIncreaseRequestURL"
target="_blank"
rel="noopener noreferrer"
>
Request Limit Increase ->
</a>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VLoader from '@/components/common/VLoader.vue';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { MetaUtils } from '@/utils/meta';
/**
* VBanner is common banner for needed pages
*/
@Component({
components: {
VLoader,
},
})
export default class InfoBar extends Vue {
public isDataFetching: boolean = true;
/**
* Lifecycle hook after initial render.
* Fetches projects.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
this.isDataFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Returns user's projects count.
*/
public get projectsCount(): number {
return this.$store.getters.projectsCount;
}
/**
* Returns project limit from store.
*/
public get projectLimit(): number {
const projectLimit: number = this.$store.getters.user.projectLimit;
if (projectLimit < this.projectsCount) return this.projectsCount;
return projectLimit;
}
/**
* Returns project limits increase request url from config.
*/
public get projectLimitsIncreaseRequestURL(): string {
return MetaUtils.getMetaContent('project-limits-increase-request-url');
}
}
</script>
<style scoped lang="scss">
.projects-info-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2582ff;
width: calc(100% - 60px);
padding: 10px 30px;
font-family: 'font_regular', sans-serif;
color: #fff;
&__info {
display: flex;
align-items: center;
&__message {
display: flex;
align-items: center;
margin-right: 5px;
font-size: 14px;
line-height: 17px;
white-space: nowrap;
&__value {
margin: 0 5px;
}
}
}
&__link {
font-size: 14px;
line-height: 17px;
font-family: 'font_medium', sans-serif;
color: #fff;
}
}
.pr-info-loader {
margin: 0 5px;
}
</style>

View File

@ -3,6 +3,7 @@
<template>
<div class="projects-list">
<InfoBar/>
<div class="projects-list__title-area">
<h2 class="projects-list__title-area__title">Projects</h2>
<VButton
@ -10,13 +11,11 @@
width="203px"
height="44px"
:on-press="onCreateClick"
:is-disabled="areProjectsFetching"
/>
</div>
<div class="projects-list__title-area__right">
</div>
<div class="projects-list-items" v-if="projectsPage.projects.length">
<VLoader v-if="areProjectsFetching" width="100px" height="100px" class="projects-loader"/>
<div class="projects-list-items" v-if="projectsPage.projects.length && !areProjectsFetching">
<SortProjectsListHeader />
<div class="projects-list-items__content">
<VList
@ -40,7 +39,9 @@ import { Component, Vue } from 'vue-property-decorator';
import VButton from '@/components/common/VButton.vue';
import VList from '@/components/common/VList.vue';
import VLoader from '@/components/common/VLoader.vue';
import VPagination from '@/components/common/VPagination.vue';
import InfoBar from '@/components/projectsList/InfoBar.vue';
import ProjectsListItem from '@/components/projectsList/ProjectsListItem.vue';
import SortProjectsListHeader from '@/components/projectsList/SortProjectsListHeader.vue';
@ -63,18 +64,24 @@ const {
VButton,
VList,
VPagination,
InfoBar,
VLoader,
},
})
export default class Projects extends Vue {
private currentPageNumber: number = 1;
private FIRST_PAGE = 1;
public areProjectsFetching: boolean = true;
/**
* Lifecycle hook after initial render where list of existing ownded projects is fetched.
* Lifecycle hook after initial render where list of existing owned projects is fetched.
*/
public async mounted(): Promise<void> {
try {
await this.$store.dispatch(FETCH_OWNED, this.currentPageNumber);
this.areProjectsFetching = false;
} catch (error) {
await this.$notify.error(`Unable to fetch owned projects. ${error.message}`);
}
@ -131,7 +138,7 @@ export default class Projects extends Vue {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, this.FIRST_PAGE);
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
this.$router.push(RouteConfig.ProjectDashboard.path);
await this.$router.push(RouteConfig.ProjectDashboard.path);
} catch (error) {
await this.$notify.error(`Unable to select project. ${error.message}`);
}
@ -172,4 +179,8 @@ export default class Projects extends Vue {
}
}
}
.projects-loader {
margin-top: 50px;
}
</style>

View File

@ -23,6 +23,7 @@
width="122px"
height="48px"
:on-press="onAddUsersClick"
:is-disabled="isAddButtonDisabled"
/>
</div>
<div class="header-selected-members" v-if="areProjectMembersSelected">
@ -105,6 +106,8 @@ export default class HeaderArea extends Vue {
private readonly headerState: ProjectMemberHeaderState;
@Prop({default: 0})
public readonly selectedProjectMembersCount: number;
@Prop({default: false})
public readonly isAddButtonDisabled: boolean;
private FIRST_PAGE = 1;

View File

@ -8,29 +8,33 @@
:header-state="headerState"
:selected-project-members-count="selectedProjectMembersLength"
@onSuccessAction="resetPaginator"
:is-add-button-disabled="areMembersFetching"
/>
</div>
<div class="team-area__container" id="team-container" v-if="isTeamAreaShown">
<SortingListHeader :on-header-click-callback="onHeaderSectionClickCallback"/>
<div class="team-area__container__content">
<VList
:data-set="projectMembers"
:item-component="getItemComponent"
:on-item-click="onMemberClick"
/>
<VLoader v-if="areMembersFetching" width="100px" height="100px"/>
<template v-else>
<div class="team-area__container" id="team-container" v-if="isTeamAreaShown">
<SortingListHeader :on-header-click-callback="onHeaderSectionClickCallback"/>
<div class="team-area__container__content">
<VList
:data-set="projectMembers"
:item-component="getItemComponent"
:on-item-click="onMemberClick"
/>
</div>
</div>
</div>
<VPagination
v-if="totalPageCount > 1"
class="pagination-area"
ref="pagination"
:total-page-count="totalPageCount"
:on-page-click-callback="onPageClick"
/>
<div class="team-area__empty-search-result-area" v-if="isEmptySearchResultShown">
<h1 class="team-area__empty-search-result-area__title">No results found</h1>
<EmptySearchResultIcon class="team-area__empty-search-result-area__image"/>
</div>
<VPagination
v-if="totalPageCount > 1"
class="pagination-area"
ref="pagination"
:total-page-count="totalPageCount"
:on-page-click-callback="onPageClick"
/>
<div class="team-area__empty-search-result-area" v-if="isEmptySearchResultShown">
<h1 class="team-area__empty-search-result-area__title">No results found</h1>
<EmptySearchResultIcon class="team-area__empty-search-result-area__image"/>
</div>
</template>
</div>
</template>
@ -38,6 +42,7 @@
import { Component, Vue } from 'vue-property-decorator';
import VList from '@/components/common/VList.vue';
import VLoader from '@/components/common/VLoader.vue';
import VPagination from '@/components/common/VPagination.vue';
import HeaderArea from '@/components/team/HeaderArea.vue';
import ProjectMemberListItem from '@/components/team/ProjectMemberListItem.vue';
@ -70,11 +75,14 @@ declare interface ResetPagination {
VPagination,
SortingListHeader,
EmptySearchResultIcon,
VLoader,
},
})
export default class ProjectMembersArea extends Vue {
private FIRST_PAGE = 1;
public areMembersFetching: boolean = true;
public $refs!: {
pagination: HTMLElement & ResetPagination;
};
@ -84,7 +92,13 @@ export default class ProjectMembersArea extends Vue {
* Fetches first page of team members list of current project.
*/
public async mounted(): Promise<void> {
await this.$store.dispatch(FETCH, 1);
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
this.areMembersFetching = false;
} catch (error) {
await this.$notify.error(error.message);
}
}
/**

View File

@ -28,7 +28,6 @@ export const appStateModule = {
isEditProfilePopupShown: false,
isChangePasswordPopupShown: false,
isPaymentSelectionShown: false,
isCreateProjectButtonShown: false,
},
satelliteName: '',
partneredSatellites: new Array<PartneredSatellite>(),
@ -100,12 +99,6 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.SHOW_DELETE_PAYMENT_METHOD_POPUP](state: any, id: string): void {
state.appState.deletePaymentMethodID = id;
},
[APP_STATE_MUTATIONS.SHOW_CREATE_PROJECT_BUTTON](state: any): void {
state.appState.isCreateProjectButtonShown = true;
},
[APP_STATE_MUTATIONS.HIDE_CREATE_PROJECT_BUTTON](state: any): void {
state.appState.isCreateProjectButtonShown = false;
},
// Mutation that closes each popup/dropdown
[APP_STATE_MUTATIONS.CLOSE_ALL](state: any): void {
state.appState.isAccountDropdownShown = false;
@ -259,12 +252,6 @@ export const appStateModule = {
commit(APP_STATE_MUTATIONS.SHOW_DELETE_PAYMENT_METHOD_POPUP, methodID);
},
[APP_STATE_ACTIONS.SHOW_CREATE_PROJECT_BUTTON]: function ({commit}: any): void {
commit(APP_STATE_MUTATIONS.SHOW_CREATE_PROJECT_BUTTON);
},
[APP_STATE_ACTIONS.HIDE_CREATE_PROJECT_BUTTON]: function ({commit}: any): void {
commit(APP_STATE_MUTATIONS.HIDE_CREATE_PROJECT_BUTTON);
},
[APP_STATE_ACTIONS.CLOSE_POPUPS]: function ({commit}: any): void {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
},

View File

@ -2,9 +2,6 @@
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import {
Project,
ProjectFields,
@ -13,7 +10,6 @@ import {
ProjectsCursor,
ProjectsPage,
} from '@/types/projects';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
export const PROJECTS_ACTIONS = {
FETCH: 'fetchProjects',
@ -170,7 +166,6 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
return project;
},
[CREATE_DEFAULT_PROJECT]: async function ({rootGetters, dispatch}: any): Promise<void> {
const FIRST_PAGE = 1;
const UNTITLED_PROJECT_NAME = 'My First Project';
const UNTITLED_PROJECT_DESCRIPTION = '___';
const project = new ProjectFields(
@ -178,17 +173,9 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
UNTITLED_PROJECT_DESCRIPTION,
rootGetters.user.id,
);
const createdProject = await dispatch(PROJECTS_ACTIONS.CREATE, project, {root: true});
const createdProject = await dispatch(CREATE, project);
await dispatch(PROJECTS_ACTIONS.SELECT, createdProject.id, {root: true});
await dispatch(PM_ACTIONS.CLEAR, null, {root: true});
await dispatch(PM_ACTIONS.FETCH, FIRST_PAGE, {root: true});
await dispatch(PAYMENTS_ACTIONS.GET_PAYMENTS_HISTORY, null, {root: true});
await dispatch(PAYMENTS_ACTIONS.GET_BALANCE, null, {root: true});
await dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP, null, {root: true});
await dispatch(PROJECTS_ACTIONS.GET_LIMITS, createdProject.id, {root: true});
await dispatch(ACCESS_GRANTS_ACTIONS.CLEAR, null, {root: true});
await dispatch(BUCKET_ACTIONS.CLEAR, null, {root: true});
await dispatch(SELECT, createdProject.id);
},
[SELECT]: function ({commit}: any, projectID: string): void {
commit(SELECT_PROJECT, projectID);

View File

@ -34,7 +34,5 @@ export const APP_STATE_MUTATIONS = {
SET_SATELLITE_NAME: 'SET_SATELLITE_NAME',
SET_PARTNERED_SATELLITES: 'SET_PARTNERED_SATELLITES',
SET_SATELLITE_STATUS: 'SET_SATELLITE_STATUS',
SHOW_CREATE_PROJECT_BUTTON: 'SHOW_CREATE_PROJECT_BUTTON',
HIDE_CREATE_PROJECT_BUTTON: 'HIDE_CREATE_PROJECT_BUTTON',
SET_COUPON_CODE_UI_STATUS: 'SET_COUPON_CODE_UI_STATUS',
};

View File

@ -28,8 +28,6 @@ export const APP_STATE_ACTIONS = {
SET_SATELLITE_NAME: 'SET_SATELLITE_NAME',
SET_PARTNERED_SATELLITES: 'SET_PARTNERED_SATELLITES',
SET_SATELLITE_STATUS: 'SET_SATELLITE_STATUS',
SHOW_CREATE_PROJECT_BUTTON: 'SHOW_CREATE_PROJECT_BUTTON',
HIDE_CREATE_PROJECT_BUTTON: 'HIDE_CREATE_PROJECT_BUTTON',
SET_COUPON_CODE_UI_STATUS: 'SET_COUPON_CODE_UI_STATUS',
};

View File

@ -19,28 +19,6 @@
<div class="dashboard__wrap__main-area">
<NavigationArea class="regular-navigation"/>
<div class="dashboard__wrap__main-area__content">
<div class="dashboard__wrap__main-area__content__bar-area">
<VInfoBar
v-if="isBillingInfoBarShown"
:first-value="storageRemaining"
:second-value="bandwidthRemaining"
first-description="of Storage Remaining"
second-description="of Bandwidth Remaining"
:path="projectDashboardPath"
:link="projectLimitsIncreaseRequestURL"
link-label="Request Limit Increase ->"
/>
<VInfoBar
v-if="isProjectLimitInfoBarShown"
is-blue="true"
:first-value="`You have used ${projectsCount}`"
first-description="of your"
:second-value="projectLimit"
second-description="available projects."
:link="projectLimitsIncreaseRequestURL"
link-label="Request Project Limit Increase"
/>
</div>
<router-view/>
</div>
</div>
@ -51,7 +29,6 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VInfoBar from '@/components/common/VInfoBar.vue';
import DashboardHeader from '@/components/header/HeaderArea.vue';
import NavigationArea from '@/components/navigation/NavigationArea.vue';
@ -60,16 +37,11 @@ import LoaderImage from '@/../static/images/common/loader.svg';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { USER_ACTIONS } from '@/store/modules/users';
import { Project } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
import {
APP_STATE_ACTIONS,
PM_ACTIONS,
} from '@/utils/constants/actionNames';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { AppState } from '@/utils/constants/appStateEnum';
import { LocalData } from '@/utils/localData';
import { MetaUtils } from '@/utils/meta';
@ -78,8 +50,6 @@ const {
GET_PAYWALL_ENABLED_STATUS,
SETUP_ACCOUNT,
GET_BALANCE,
GET_CREDIT_CARDS,
GET_PAYMENTS_HISTORY,
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
} = PAYMENTS_ACTIONS;
@ -87,18 +57,10 @@ const {
components: {
NavigationArea,
DashboardHeader,
VInfoBar,
LoaderImage,
},
})
export default class DashboardArea extends Vue {
private FIRST_PAGE: number = 1;
/**
* Holds router link to project dashboard page.
*/
public readonly projectDashboardPath: string = RouteConfig.ProjectDashboard.path;
/**
* Lifecycle hook before initial render.
* Sets access grants web worker.
@ -117,7 +79,6 @@ export default class DashboardArea extends Vue {
* Pre fetches user`s and project information.
*/
public async mounted(): Promise<void> {
// TODO: combine all project related requests in one
try {
await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
@ -143,30 +104,6 @@ export default class DashboardArea extends Vue {
await this.$notify.error(`Unable to setup account. ${error.message}`);
}
try {
await this.$store.dispatch(GET_BALANCE);
} catch (error) {
await this.$notify.error(`Unable to get account balance. ${error.message}`);
}
try {
await this.$store.dispatch(GET_CREDIT_CARDS);
} catch (error) {
await this.$notify.error(`Unable to get credit cards. ${error.message}`);
}
try {
await this.$store.dispatch(GET_PAYMENTS_HISTORY);
} catch (error) {
await this.$notify.error(`Unable to get account payments history. ${error.message}`);
}
try {
await this.$store.dispatch(GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
} catch (error) {
await this.$notify.error(`Unable to get usage and charges for current billing period. ${error.message}`);
}
let projects: Project[] = [];
try {
@ -191,43 +128,6 @@ export default class DashboardArea extends Vue {
this.selectProject(projects);
try {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch access grants. ${error.message}`);
}
try {
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_OWNED, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch owned projects. ${error.message}`);
}
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH_ALL_BUCKET_NAMES);
} catch (error) {
await this.$notify.error(`Unable to fetch all bucket names. ${error.message}`);
}
await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
try {
await this.$store.dispatch(PM_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch project members. ${error.message}`);
}
try {
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
} catch (error) {
await this.$notify.error(`Unable to fetch project limits. ${error.message}`);
}
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch buckets. ${error.message}`);
}
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
}
@ -259,80 +159,6 @@ export default class DashboardArea extends Vue {
return this.$store.state.appStateModule.isBetaSatellite;
}
/**
* Indicates if billing info bar is shown.
*/
public get isBillingInfoBarShown(): boolean {
const showBillingInfoBar = (this.$route.name === RouteConfig.Billing.name) || (this.$route.name === RouteConfig.ProjectDashboard.name);
return showBillingInfoBar && this.projectsCount > 0;
}
/**
* Indicates if project limit info bar is shown.
*/
public get isProjectLimitInfoBarShown(): boolean {
return this.$route.name === RouteConfig.ProjectsList.name;
}
/**
* Returns user's projects count.
*/
public get projectsCount(): number {
return this.$store.getters.projectsCount;
}
/**
* Returns project limit from store.
*/
public get projectLimit(): number {
const projectLimit: number = this.$store.getters.user.projectLimit;
if (projectLimit < this.projectsCount) return this.projectsCount;
return projectLimit;
}
/**
* Returns project limits increase request url from config.
*/
public get projectLimitsIncreaseRequestURL(): string {
return MetaUtils.getMetaContent('project-limits-increase-request-url');
}
/**
* Returns formatted string of remaining storage.
*/
public get storageRemaining(): string {
const storageUsed = this.$store.state.projectsModule.currentLimits.storageUsed;
const storageLimit = this.$store.state.projectsModule.currentLimits.storageLimit;
const difference = storageLimit - storageUsed;
if (difference < 0) {
return '0 Bytes';
}
const remaining = new Size(difference, 2);
return `${remaining.formattedBytes}${remaining.label}`;
}
/**
* Returns formatted string of remaining bandwidth.
*/
public get bandwidthRemaining(): string {
const bandwidthUsed = this.$store.state.projectsModule.currentLimits.bandwidthUsed;
const bandwidthLimit = this.$store.state.projectsModule.currentLimits.bandwidthLimit;
const difference = bandwidthLimit - bandwidthUsed;
if (difference < 0) {
return '0 Bytes';
}
const remaining = new Size(difference, 2);
return `${remaining.formattedBytes}${remaining.label}`;
}
/**
* Indicates if loading screen is active.
*/
@ -379,7 +205,7 @@ export default class DashboardArea extends Vue {
left: 0;
right: 0;
bottom: 0;
background-color: rgba(134, 134, 148, 1);
background-color: rgba(134, 134, 148, 0.3);
visibility: hidden;
opacity: 0;
-webkit-transition: all 0.5s linear;
@ -445,10 +271,7 @@ export default class DashboardArea extends Vue {
overflow-y: scroll;
height: calc(100vh - 62px);
width: 100%;
&__bar-area {
position: relative;
}
position: relative;
}
}
}

View File

@ -8,10 +8,12 @@ import DetailedHistory from '@/components/account/billing/depositAndBillingHisto
import { PaymentsHttpApi } from '@/api/payments';
import { router } from '@/router';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { makePaymentsModule, PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { makeProjectsModule, PROJECTS_MUTATIONS } from '@/store/modules/projects';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
import { Project } from '@/types/projects';
import { Notificator } from '@/utils/plugins/notificator';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { ProjectsApiMock } from '../../../mock/api/projects';
@ -21,6 +23,7 @@ const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const paymentsApi = new PaymentsHttpApi();
const paymentsModule = makePaymentsModule(paymentsApi);
const notificationsModule = makeNotificationsModule();
const itemInvoice = new PaymentsHistoryItem('testId', 'Invoice', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Invoice);
const itemCharge = new PaymentsHistoryItem('testId1', 'Charge', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Charge);
const itemTransaction = new PaymentsHistoryItem('testId2', 'Transaction', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Transaction);
@ -29,23 +32,34 @@ const project = new Project('id', 'projectName', 'projectDescription', 'test', '
localVue.use(Vuex);
const store = new Vuex.Store({ modules: { paymentsModule, projectsModule }});
const store = new Vuex.Store({ modules: { paymentsModule, projectsModule, notificationsModule }});
store.commit(PROJECTS_MUTATIONS.SET_PROJECTS, [project]);
store.commit(PROJECTS_MUTATIONS.SELECT_PROJECT, project.id);
class NotificatorPlugin {
public install() {
localVue.prototype.$notify = new Notificator(store);
}
}
const notificationsPlugin = new NotificatorPlugin();
localVue.use(notificationsPlugin);
describe('DetailedHistory', (): void => {
it('renders correctly without items', (): void => {
it('renders correctly without items', async (): Promise<void> => {
const wrapper = shallowMount(DetailedHistory, {
localVue,
store,
router,
});
await wrapper.setData({ isDataFetching: false });
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with deposit items', (): void => {
store.commit(PAYMENTS_MUTATIONS.SET_PAYMENTS_HISTORY, [itemTransaction, itemTransaction1, itemInvoice, itemCharge]);
it('renders correctly with deposit items', async (): Promise<void> => {
await store.commit(PAYMENTS_MUTATIONS.SET_PAYMENTS_HISTORY, [itemTransaction, itemTransaction1, itemInvoice, itemCharge]);
const wrapper = shallowMount(DetailedHistory, {
localVue,
@ -53,6 +67,8 @@ describe('DetailedHistory', (): void => {
router,
});
await wrapper.setData({ isDataFetching: false });
expect(wrapper).toMatchSnapshot();
});

View File

@ -57,10 +57,13 @@ describe('EstimatedCostsAndCredits', (): void => {
localVue,
});
await wrapper.setData({ isDataFetching: false });
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with project and project usage and charges', async (): Promise<void> => {
await store.commit(PROJECTS_MUTATIONS.ADD, project);
await store.commit(SET_PROJECT_USAGE_AND_CHARGES, [projectCharge]);
await store.commit(SET_PRICE_SUMMARY, [projectCharge]);
@ -69,10 +72,13 @@ describe('EstimatedCostsAndCredits', (): void => {
localVue,
});
await wrapper.setData({ isDataFetching: false });
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with 2 projects and project usage and charges', async (): Promise<void> => {
await store.commit(PROJECTS_MUTATIONS.ADD, project);
await store.commit(PROJECTS_MUTATIONS.ADD, project1);
await store.commit(SET_PROJECT_USAGE_AND_CHARGES, [projectCharge, projectCharge1]);
await store.commit(SET_PRICE_SUMMARY, [projectCharge, projectCharge1]);
@ -82,6 +88,8 @@ describe('EstimatedCostsAndCredits', (): void => {
localVue,
});
await wrapper.setData({ isDataFetching: false });
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -9,10 +9,12 @@ import CreditsHistory from '@/components/account/billing/freeCredits/CreditsHist
import { PaymentsHttpApi } from '@/api/payments';
import { router } from '@/router';
import { appStateModule } from '@/store/modules/appState';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { makePaymentsModule, PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { makeProjectsModule, PROJECTS_MUTATIONS } from '@/store/modules/projects';
import { PaymentsHistoryItem, PaymentsHistoryItemType } from '@/types/payments';
import { Project } from '@/types/projects';
import { Notificator } from '@/utils/plugins/notificator';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ProjectsApiMock } from '../../../mock/api/projects';
@ -22,32 +24,43 @@ const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const paymentsApi = new PaymentsHttpApi();
const paymentsModule = makePaymentsModule(paymentsApi);
const notificationsModule = makeNotificationsModule();
const itemInvoice = new PaymentsHistoryItem('testId', 'Invoice', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Invoice);
const itemCharge = new PaymentsHistoryItem('testId1', 'Charge', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Charge);
const itemTransaction = new PaymentsHistoryItem('testId2', 'Transaction', 500, 500, 'test', 'test', new Date(1), new Date(1), PaymentsHistoryItemType.Transaction);
const coupon = new PaymentsHistoryItem('testId', 'desc', 275, 0, 'test', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 275);
const coupon1 = new PaymentsHistoryItem('testId', 'desc', 500, 0, 'test', '', new Date(1), new Date(1), PaymentsHistoryItemType.Coupon, 300);
const project = new Project('id', 'projectName', 'projectDescription', 'test', 'testOwnerId', false);
const clickSpy = sinon.spy();
localVue.use(Vuex);
localVue.filter('centsToDollars', (cents: number): string => {
return `$${(cents / 100).toFixed(2)}`;
});
const store = new Vuex.Store({ modules: { paymentsModule, projectsModule, appStateModule }});
const store = new Vuex.Store({ modules: { paymentsModule, projectsModule, appStateModule, notificationsModule }});
store.commit(PROJECTS_MUTATIONS.SET_PROJECTS, [project]);
store.commit(PROJECTS_MUTATIONS.SELECT_PROJECT, project.id);
store.commit(PAYMENTS_MUTATIONS.SET_PAYMENTS_HISTORY, [itemInvoice, itemCharge, itemTransaction, coupon, coupon1]);
class NotificatorPlugin {
public install() {
localVue.prototype.$notify = new Notificator(store);
}
}
const notificationsPlugin = new NotificatorPlugin();
localVue.use(notificationsPlugin);
describe('CreditsHistory', (): void => {
it('renders correctly', (): void => {
it('renders correctly', async (): Promise<void> => {
const wrapper = shallowMount(CreditsHistory, {
localVue,
store,
router,
});
await wrapper.setData({ isHistoryFetching: false });
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -45,12 +45,14 @@ localVue.use(notificationsPlugin);
const ANIMATION_COMPLETE_TIME = 600;
describe('PaymentMethods', () => {
it('renders correctly without card', () => {
it('renders correctly without card', async (): Promise<void> => {
const wrapper = mount(PaymentMethods, {
store,
localVue,
});
await wrapper.setData({ areCardsFetching: false });
expect(wrapper).toMatchSnapshot();
});
@ -60,6 +62,8 @@ describe('PaymentMethods', () => {
localVue,
});
await wrapper.setData({ areCardsFetching: false });
await wrapper.find('.add-card-button').trigger('click');
await new Promise(resolve => {
@ -82,6 +86,8 @@ describe('PaymentMethods', () => {
localVue,
});
await wrapper.setData({ areCardsFetching: false });
await wrapper.find('.add-storj-button').trigger('click');
await new Promise(resolve => {
@ -98,7 +104,7 @@ describe('PaymentMethods', () => {
});
});
it('renders correctly with card', () => {
it('renders correctly with card', async (): Promise<void> => {
const card = new CreditCard('cardId', 12, 2100, 'test', '0000', true);
store.commit(PAYMENTS_MUTATIONS.SET_CREDIT_CARDS, [card]);
@ -107,6 +113,8 @@ describe('PaymentMethods', () => {
localVue,
});
await wrapper.setData({ areCardsFetching: false });
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -57,6 +57,7 @@ exports[`PaymentMethods renders correctly after add STORJ and cancel click 1`] =
<!---->
<!---->
<!---->
<!---->
</div>
`;
@ -96,6 +97,7 @@ exports[`PaymentMethods renders correctly after add STORJ and cancel click 2`] =
<!---->
<!---->
<!---->
<!---->
</div>
`;
@ -125,6 +127,7 @@ exports[`PaymentMethods renders correctly after add card and cancel click 1`] =
</div>
<!---->
<!---->
<!---->
</div>
`;
@ -150,6 +153,7 @@ exports[`PaymentMethods renders correctly after add card and cancel click 2`] =
<!---->
<!---->
<!---->
<!---->
</div>
`;
@ -190,6 +194,7 @@ exports[`PaymentMethods renders correctly with card 1`] = `
</div>
</div>
<!---->
<!---->
<div class="payment-methods-area__existing-cards-container">
<div class="payment-methods-container__card-container">
<div class="payment-methods-container__card-container__info-area">
@ -257,5 +262,6 @@ exports[`PaymentMethods renders correctly without card 1`] = `
<!---->
<!---->
<!---->
<!---->
</div>
`;

View File

@ -1,31 +0,0 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import VInfoBar from '@/components/common/VInfoBar.vue';
import { router } from '@/router';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('VInfoBar.vue', () => {
it('renders correctly', () => {
const wrapper = shallowMount(VInfoBar, {
propsData: {
firstValue: '5GB',
secondValue: '5GB',
firstDescription: 'test1',
secondDescription: 'test2',
link: 'testlink',
linkLabel: 'label',
path: '/',
},
router,
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VInfoBar.vue renders correctly 1`] = `
<div class="info-bar">
<div class="info-bar__info-area"><b class="info-bar__info-area__first-value">5GB</b> <span class="info-bar__info-area__first-description">test1</span> <b class="info-bar__info-area__second-value">5GB</b> <span class="info-bar__info-area__second-description">test2</span>
<router-link-stub to="/" tag="a" ariacurrentvalue="page" event="click" class="info-bar__info-area__button">Details</router-link-stub>
</div> <a href="testlink" target="_blank" rel="noopener noreferrer" class="info-bar__link">
label
</a>
</div>
`;

View File

@ -16,7 +16,7 @@ export class AccessGrantsMock implements AccessGrantsApi {
private readonly date = new Date(0);
private mockAccessGrantsPage: AccessGrantsPage;
public setMockApiKeysPage(mockAccessGrantsPage: AccessGrantsPage): void {
public setMockAccessGrantsPage(mockAccessGrantsPage: AccessGrantsPage): void {
this.mockAccessGrantsPage = mockAccessGrantsPage;
}

View File

@ -7,7 +7,7 @@ import { Project, ProjectFields, ProjectLimits, ProjectsApi, ProjectsCursor, Pro
* Mock for ProjectsApi
*/
export class ProjectsApiMock implements ProjectsApi {
private mockProjects: Project[];
private mockProjects: Project[] = [];
private mockLimits: ProjectLimits;
private mockProjectsPage: ProjectsPage;

View File

@ -5,28 +5,47 @@ import Vuex from 'vuex';
import ProjectDashboard from '@/components/project/ProjectDashboard.vue';
import { makeAccessGrantsModule } from '@/store/modules/accessGrants';
import { appStateModule } from '@/store/modules/appState';
import { makeBucketsModule } from '@/store/modules/buckets';
import { makePaymentsModule } from '@/store/modules/payments';
import { makeProjectMembersModule } from '@/store/modules/projectMembers';
import { makeProjectsModule, PROJECTS_MUTATIONS } from '@/store/modules/projects';
import { makeUsersModule } from '@/store/modules/users';
import { AccessGrantsPage } from '@/types/accessGrants';
import { ProjectMembersPage } from '@/types/projectMembers';
import { Project } from '@/types/projects';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { AccessGrantsMock } from '../mock/api/accessGrants';
import { BucketsMock } from '../mock/api/buckets';
import { PaymentsMock } from '../mock/api/payments';
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
import { ProjectsApiMock } from '../mock/api/projects';
import { UsersApiMock } from '../mock/api/users';
const localVue = createLocalVue();
localVue.use(Vuex);
const usersApi = new UsersApiMock();
const usersModule = makeUsersModule(usersApi);
const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const bucketsApi = new BucketsMock();
const bucketsModule = makeBucketsModule(bucketsApi);
const paymentsApi = new PaymentsMock();
const paymentsModule = makePaymentsModule(paymentsApi);
const membersApi = new ProjectMembersApiMock();
const membersModule = makeProjectMembersModule(membersApi);
const grantsApi = new AccessGrantsMock();
const grantsModule = makeAccessGrantsModule(grantsApi);
const store = new Vuex.Store({ modules: { appStateModule, projectsModule, bucketsModule, paymentsModule, membersModule, grantsModule }});
const store = new Vuex.Store({ modules: { appStateModule, usersModule, projectsModule }});
const project = new Project('id', 'test', 'test', 'test', 'ownedId', false);
const membersPage = new ProjectMembersPage();
membersApi.setMockPage(membersPage);
const grantsPage = new AccessGrantsPage();
grantsApi.setMockAccessGrantsPage(grantsPage);
describe('ProjectDashboard.vue', () => {
it('renders correctly', (): void => {
it('renders correctly', async (): Promise<void> => {
store.commit(PROJECTS_MUTATIONS.ADD, project);
store.commit(PROJECTS_MUTATIONS.SELECT_PROJECT, project.id);
@ -35,6 +54,11 @@ describe('ProjectDashboard.vue', () => {
localVue,
});
await wrapper.setData({
areBucketsFetching: false,
isSummaryDataFetching: false,
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -47,6 +47,9 @@ describe('ProjectSummary.vue', (): void => {
const wrapper = mount(ProjectSummary, {
store,
localVue,
propsData: {
isDataFetching: false,
},
});
expect(wrapper).toMatchSnapshot();

View File

@ -15,6 +15,7 @@ describe('UsageArea.vue', () => {
title: 'test Title',
used: 500000000,
limit: 1000000000,
isDataFetching: false,
},
});
@ -28,6 +29,7 @@ describe('UsageArea.vue', () => {
title: 'test Title',
used: 1000000000,
limit: 500000000,
isDataFetching: false,
},
});

View File

@ -2,7 +2,7 @@
exports[`ProjectUsage.vue renders correctly 1`] = `
<div class="project-usage">
<usagearea-stub title="Storage" used="100" limit="1000" class="project-usage__storage-used"></usagearea-stub>
<usagearea-stub title="Bandwidth" used="100" limit="1000" class="project-usage__bandwidth-used"></usagearea-stub>
<usagearea-stub title="Storage" used="100" limit="1000" isdatafetching="true" class="project-usage__storage-used"></usagearea-stub>
<usagearea-stub title="Bandwidth" used="100" limit="1000" isdatafetching="true" class="project-usage__bandwidth-used"></usagearea-stub>
</div>
`;

View File

@ -39,12 +39,14 @@ describe('ProjectMembersArea.vue', () => {
pmApi.setMockPage(testProjectMembersPage);
it('renders correctly', () => {
it('renders correctly', async (): Promise<void> => {
const wrapper = shallowMount(ProjectMembersArea, {
store,
localVue,
});
await wrapper.setData({ areMembersFetching: false });
expect(wrapper).toMatchSnapshot();
});
@ -59,7 +61,7 @@ describe('ProjectMembersArea.vue', () => {
expect(wrapper.vm.projectMembers).toEqual([projectMember1]);
});
it('team area renders correctly', () => {
it('team area renders correctly', async (): Promise<void> => {
store.commit(FETCH, testProjectMembersPage);
const wrapper = shallowMount(ProjectMembersArea, {
@ -67,6 +69,8 @@ describe('ProjectMembersArea.vue', () => {
localVue,
});
await wrapper.setData({ areMembersFetching: false });
const emptySearchResultArea = wrapper.findAll('.team-area__empty-search-result-area');
expect(emptySearchResultArea.length).toBe(0);
@ -138,7 +142,7 @@ describe('ProjectMembersArea.vue', () => {
expect(wrapper.vm.projectMembers[0].user.id).toBe(projectMember2.user.id);
});
it('empty search result area render correctly', () => {
it('empty search result area render correctly', async (): Promise<void> => {
const testPage1 = new ProjectMembersPage();
testPage1.projectMembers = [];
testPage1.totalCount = 0;
@ -153,6 +157,8 @@ describe('ProjectMembersArea.vue', () => {
localVue,
});
await wrapper.setData({ areMembersFetching: false });
const emptySearchResultArea = wrapper.findAll('.team-area__empty-search-result-area');
expect(emptySearchResultArea.length).toBe(1);

View File

@ -19,12 +19,16 @@ exports[`ProjectMembersArea.vue renders correctly 1`] = `
<div class="team-area__header">
<headerarea-stub headerstate="0" selectedprojectmemberscount="0"></headerarea-stub>
</div>
<!---->
<!---->
<div class="team-area__empty-search-result-area">
<h1 class="team-area__empty-search-result-area__title">No results found</h1>
<emptysearchresulticon-stub class="team-area__empty-search-result-area__image"></emptysearchresulticon-stub>
<div id="team-container" class="team-area__container">
<sortinglistheader-stub onheaderclickcallback="function () { [native code] }"></sortinglistheader-stub>
<div class="team-area__container__content">
<vlist-stub itemcomponent="function VueComponent (options) {
this._init(options);
}" onitemclick="function () { [native code] }" dataset="[object Object]"></vlist-stub>
</div>
</div>
<!---->
<!---->
</div>
`;

View File

@ -9,10 +9,6 @@ exports[`Dashboard renders correctly when data is loaded 1`] = `
<div class="dashboard__wrap__main-area">
<navigationarea-stub class="regular-navigation"></navigationarea-stub>
<div class="dashboard__wrap__main-area__content">
<div class="dashboard__wrap__main-area__content__bar-area">
<!---->
<!---->
</div>
<router-view-stub name="default"></router-view-stub>
</div>
</div>