web/multinode: expectations block added
added displaying of undistributed balance and current month expectations contains changes from https://review.dev.storj.io/c/storj/storj/+/4867 to be workable Change-Id: I9cb00d2db5b819a71ceeddf91d6863a6b4fa9034
This commit is contained in:
parent
64e4225721
commit
20b98d31b8
@ -2,7 +2,7 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { APIClient } from '@/api/index';
|
||||
import { NodePayoutsSummary, PayoutsSummary } from '@/payouts';
|
||||
import { Expectations, NodePayoutsSummary, PayoutsSummary } from '@/payouts';
|
||||
|
||||
/**
|
||||
* client for nodes controller of MND api.
|
||||
@ -11,7 +11,7 @@ export class PayoutsClient extends APIClient {
|
||||
private readonly ROOT_PATH: string = '/api/v0/payouts';
|
||||
|
||||
/**
|
||||
* handles fetch of payouts summary information.
|
||||
* Handles fetch of payouts summary information.
|
||||
*
|
||||
* @param satelliteId - satellite id.
|
||||
* @param period - selected period.
|
||||
@ -26,7 +26,13 @@ export class PayoutsClient extends APIClient {
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async summary(satelliteId: string | null, period: string | null): Promise<PayoutsSummary> {
|
||||
let path = `${this.ROOT_PATH}/summary`;
|
||||
let path = `${this.ROOT_PATH}/`;
|
||||
|
||||
if (satelliteId) {
|
||||
path += `/satellites/${satelliteId}`;
|
||||
}
|
||||
|
||||
path += '/summaries';
|
||||
|
||||
if (period) {
|
||||
path += `/${period}`;
|
||||
@ -52,4 +58,39 @@ export class PayoutsClient extends APIClient {
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fetch of payouts expectation such as estimated current month payout and undistributed payout.
|
||||
*
|
||||
* @param nodeId - node id.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async expectations(nodeId: string | null): Promise<Expectations> {
|
||||
let path = `${this.ROOT_PATH}/expectations`;
|
||||
|
||||
if (nodeId) {
|
||||
path += `/${nodeId}`;
|
||||
}
|
||||
|
||||
const response = await this.http.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return new Expectations(
|
||||
result.currentMonthEstimation,
|
||||
result.undistributed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
85
web/multinode/src/app/components/payouts/BalanceArea.vue
Normal file
85
web/multinode/src/app/components/payouts/BalanceArea.vue
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<info-block>
|
||||
<div class="balance-area" slot="body">
|
||||
<header class="balance-area__header">
|
||||
<h3 class="balance-area__header__title">Balance</h3>
|
||||
</header>
|
||||
<div class="balance-area__content">
|
||||
<div class="balance-area__content__item">
|
||||
<p class="balance-area__content__item__label">Undistributed Payouts</p>
|
||||
<p class="balance-area__content__item__value">{{ undistributed | centsToDollars }}</p>
|
||||
</div>
|
||||
<div class="balance-area__content__item">
|
||||
<p class="balance-area__content__item__label">Current Month Estimated Earnings</p>
|
||||
<p class="balance-area__content__item__value">{{ currentMonthEstimation | centsToDollars }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</info-block>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import InfoBlock from '../common/InfoBlock.vue';
|
||||
|
||||
@Component({
|
||||
components: { InfoBlock },
|
||||
})
|
||||
export default class BalanceArea extends Vue {
|
||||
@Prop({default: 0})
|
||||
public undistributed: number;
|
||||
@Prop({default: 0})
|
||||
public currentMonthEstimation: number;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.balance-area {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
&__header {
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: var(--c-title);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 34px;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
color: var(--c-gray);
|
||||
font-family: 'font_medium', sans-serif;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 18px;
|
||||
color: var(--c-title);
|
||||
font-family: 'font_semiBold', sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -5,7 +5,7 @@ import { ActionContext, ActionTree, GetterTree, Module, MutationTree } from 'vue
|
||||
|
||||
import { RootState } from '@/app/store/index';
|
||||
import { monthNames } from '@/app/types/date';
|
||||
import { PayoutsSummary } from '@/payouts';
|
||||
import { Expectations, PayoutsSummary } from '@/payouts';
|
||||
import { Payouts } from '@/payouts/service';
|
||||
|
||||
/**
|
||||
@ -14,6 +14,8 @@ import { Payouts } from '@/payouts/service';
|
||||
export class PayoutsState {
|
||||
public summary: PayoutsSummary = new PayoutsSummary();
|
||||
public selectedPayoutPeriod: string | null = null;
|
||||
public selectedNodeExpectations: Expectations = new Expectations();
|
||||
public totalExpectations: Expectations = new Expectations();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,9 +38,12 @@ export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
this.mutations = {
|
||||
setSummary: this.setSummary,
|
||||
setPayoutPeriod: this.setPayoutPeriod,
|
||||
setCurrentNodeExpectations: this.setCurrentNodeExpectations,
|
||||
setTotalExpectation: this.setTotalExpectation,
|
||||
};
|
||||
this.actions = {
|
||||
summary: this.summary.bind(this),
|
||||
expectations: this.expectations.bind(this),
|
||||
};
|
||||
this.getters = {
|
||||
periodString: this.periodString,
|
||||
@ -47,7 +52,7 @@ export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
|
||||
// Mutations
|
||||
/**
|
||||
* populate mutation will set payouts state.
|
||||
* setSummary mutation will set payouts summary state.
|
||||
* @param state - state of the module.
|
||||
* @param summary - payouts summary information depends on selected time and satellite.
|
||||
*/
|
||||
@ -64,6 +69,24 @@ export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
state.selectedPayoutPeriod = period;
|
||||
}
|
||||
|
||||
/**
|
||||
* setCurrentNodeExpectations mutation will set payouts expectation for selected node.
|
||||
* @param state - state of the module.
|
||||
* @param expectations - payouts summary information depends on selected time and satellite.
|
||||
*/
|
||||
public setCurrentNodeExpectations(state: PayoutsState, expectations: Expectations): void {
|
||||
state.selectedNodeExpectations = expectations;
|
||||
}
|
||||
|
||||
/**
|
||||
* setTotalExpectation mutation will set total payouts expectation for all nodes.
|
||||
* @param state - state of the module.
|
||||
* @param expectations - payouts summary information depends on selected time and satellite.
|
||||
*/
|
||||
public setTotalExpectation(state: PayoutsState, expectations: Expectations): void {
|
||||
state.totalExpectations = expectations;
|
||||
}
|
||||
|
||||
// Actions
|
||||
/**
|
||||
* summary action loads payouts summary information.
|
||||
@ -77,6 +100,17 @@ export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
ctx.commit('setSummary', summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* expectations action loads payouts total or by node payout expectation information.
|
||||
* @param ctx - context of the Vuex action.
|
||||
* @param nodeId - node id.
|
||||
*/
|
||||
public async expectations(ctx: ActionContext<PayoutsState, RootState>, nodeId: string | null): Promise<void> {
|
||||
const expectations = await this.payouts.expectations(nodeId);
|
||||
|
||||
ctx.commit(`${nodeId ? 'setCurrentNodeExpectations' : 'setTotalExpectation'}`, expectations);
|
||||
}
|
||||
|
||||
// Getters
|
||||
/**
|
||||
* periodString is full name month and year representation of selected payout period.
|
||||
|
@ -12,17 +12,21 @@
|
||||
</div>
|
||||
<payouts-summary-table
|
||||
class="payouts__left-area__table"
|
||||
v-if="payoutsSummary.nodeSummary"
|
||||
:node-payouts-summary="payoutsSummary.nodeSummary"
|
||||
v-if="payouts.summary.nodeSummary"
|
||||
:node-payouts-summary="payouts.summary.nodeSummary"
|
||||
/>
|
||||
</div>
|
||||
<div class="payouts__right-area">
|
||||
<details-area
|
||||
:total-earned="payoutsSummary.totalEarned"
|
||||
:total-held="payoutsSummary.totalHeld"
|
||||
:total-paid="payoutsSummary.totalPaid"
|
||||
:total-earned="payouts.summary.totalEarned"
|
||||
:total-held="payouts.summary.totalHeld"
|
||||
:total-paid="payouts.summary.totalPaid"
|
||||
:period="period"
|
||||
/>
|
||||
<balance-area
|
||||
:current-month-estimation="payouts.totalExpectations.currentMonthEstimation"
|
||||
:undistributed="payouts.totalExpectations.undistributed"
|
||||
/>
|
||||
<!-- <payout-history-block />-->
|
||||
</div>
|
||||
</div>
|
||||
@ -34,16 +38,18 @@ import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import SatelliteSelectionDropdown from '@/app/components/common/SatelliteSelectionDropdown.vue';
|
||||
import NodesTable from '@/app/components/myNodes/tables/NodesTable.vue';
|
||||
import BalanceArea from '@/app/components/payouts/BalanceArea.vue';
|
||||
import DetailsArea from '@/app/components/payouts/DetailsArea.vue';
|
||||
import PayoutHistoryBlock from '@/app/components/payouts/PayoutHistoryBlock.vue';
|
||||
import PayoutPeriodCalendarButton from '@/app/components/payouts/PayoutPeriodCalendarButton.vue';
|
||||
import PayoutsSummaryTable from '@/app/components/payouts/tables/payoutSummary/PayoutsSummaryTable.vue';
|
||||
|
||||
import { UnauthorizedError } from '@/api';
|
||||
import { PayoutsSummary } from '@/payouts';
|
||||
import { PayoutsState } from '@/app/store/payouts';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
BalanceArea,
|
||||
PayoutPeriodCalendarButton,
|
||||
PayoutHistoryBlock,
|
||||
DetailsArea,
|
||||
@ -63,13 +69,23 @@ export default class PayoutsPage extends Vue {
|
||||
|
||||
// TODO: notify error
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('payouts/expectations');
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// TODO: redirect to login screen.
|
||||
}
|
||||
|
||||
// TODO: notify error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* payoutsSummary contains payouts summary from store.
|
||||
* payoutsSummary contains payouts state from store.
|
||||
*/
|
||||
public get payoutsSummary(): PayoutsSummary {
|
||||
return this.$store.state.payouts.summary;
|
||||
public get payouts(): PayoutsState {
|
||||
return this.$store.state.payouts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,3 +75,20 @@ export class PayoutPeriod {
|
||||
return new PayoutPeriod(parseInt(periodArray[0]), parseInt(periodArray[1]) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PayoutsSummary is a representation of current month estimated payout and undistributed payouts.
|
||||
*/
|
||||
export class Expectations {
|
||||
public constructor(
|
||||
public currentMonthEstimation: number = 0,
|
||||
public undistributed: number = 0,
|
||||
) {
|
||||
this.currentMonthEstimation = this.convertToCents(this.currentMonthEstimation);
|
||||
this.undistributed = this.convertToCents(this.undistributed);
|
||||
}
|
||||
|
||||
private convertToCents(value: number): number {
|
||||
return value / PRICE_DIVIDER;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { PayoutsClient } from '@/api/payouts';
|
||||
import { PayoutsSummary } from '@/payouts/index';
|
||||
import { Expectations, PayoutsSummary } from '@/payouts/index';
|
||||
|
||||
/**
|
||||
* exposes all payouts related logic
|
||||
@ -32,4 +32,22 @@ export class Payouts {
|
||||
public async summary(satelliteId: string | null, period: string | null): Promise<PayoutsSummary> {
|
||||
return await this.payouts.summary(satelliteId, period);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetches of payouts expectation such as estimated current month payout and undistributed payout.
|
||||
*
|
||||
* @param nodeId - node id.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async expectations(nodeId: string | null): Promise<Expectations> {
|
||||
return await this.payouts.expectations(nodeId);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import BalanceArea from '@/app/components/payouts/BalanceArea.vue';
|
||||
|
||||
import { Currency } from '@/app/utils/currency';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
localVue.filter('centsToDollars', (cents: number): string => {
|
||||
return Currency.dollarsFromCents(cents);
|
||||
});
|
||||
|
||||
describe('BalanceArea', (): void => {
|
||||
it('renders correctly', (): void => {
|
||||
const wrapper = shallowMount(BalanceArea, {
|
||||
localVue,
|
||||
propsData: {
|
||||
currentMonthEstimation: 66000,
|
||||
undistributed: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BalanceArea renders correctly 1`] = `
|
||||
<info-block-stub>
|
||||
<div class="balance-area">
|
||||
<header class="balance-area__header">
|
||||
<h3 class="balance-area__header__title">Balance</h3>
|
||||
</header>
|
||||
<div class="balance-area__content">
|
||||
<div class="balance-area__content__item">
|
||||
<p class="balance-area__content__item__label">Undistributed Payouts</p>
|
||||
<p class="balance-area__content__item__value">$10.00</p>
|
||||
</div>
|
||||
<div class="balance-area__content__item">
|
||||
<p class="balance-area__content__item__label">Current Month Estimated Earnings</p>
|
||||
<p class="balance-area__content__item__value">$660.00</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</info-block-stub>
|
||||
`;
|
@ -13,6 +13,7 @@ exports[`PayoutsPage renders correctly 1`] = `
|
||||
</div>
|
||||
<div class="payouts__right-area">
|
||||
<details-area-stub totalearned="0" totalheld="0" totalpaid="0" period="All time"></details-area-stub>
|
||||
<balance-area-stub undistributed="0" currentmonthestimation="0"></balance-area-stub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { PayoutsSummary } from '@/payouts';
|
||||
import { Expectations, PayoutsSummary } from '@/payouts';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import store, { payoutsService } from '../mock/store';
|
||||
@ -11,6 +11,8 @@ import store, { payoutsService } from '../mock/store';
|
||||
const state = store.state as any;
|
||||
|
||||
const summary = new PayoutsSummary(5000000, 6000000, 9000000);
|
||||
const expectations = new Expectations(4000000, 3000000);
|
||||
const expectationsByNode = new Expectations(1000000, 2000000);
|
||||
const period = '2021-04';
|
||||
|
||||
describe('mutations', () => {
|
||||
@ -29,12 +31,27 @@ describe('mutations', () => {
|
||||
|
||||
expect(state.payouts.selectedPayoutPeriod).toBe(period);
|
||||
});
|
||||
|
||||
it('sets total expectations', () => {
|
||||
store.commit('payouts/setTotalExpectation', expectations);
|
||||
|
||||
expect(state.payouts.totalExpectations.currentMonthEstimation).toBe(expectations.currentMonthEstimation);
|
||||
expect(state.payouts.selectedNodeExpectations.currentMonthEstimation).toBe(0);
|
||||
});
|
||||
|
||||
it('sets selected node expectations', () => {
|
||||
store.commit('payouts/setCurrentNodeExpectations', expectationsByNode);
|
||||
|
||||
expect(state.payouts.selectedNodeExpectations.currentMonthEstimation).toBe(expectationsByNode.currentMonthEstimation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
store.commit('payouts/setSummary', new PayoutsSummary());
|
||||
store.commit('payouts/setCurrentNodeExpectations', new Expectations());
|
||||
store.commit('payouts/setTotalExpectation', new Expectations());
|
||||
});
|
||||
|
||||
it('throws error on failed summary fetch', async () => {
|
||||
@ -57,6 +74,38 @@ describe('actions', () => {
|
||||
|
||||
expect(state.payouts.summary.totalPaid).toBe(summary.totalPaid);
|
||||
});
|
||||
|
||||
it('throws error on failed expectations fetch', async () => {
|
||||
jest.spyOn(payoutsService, 'expectations').mockImplementation(() => { throw new Error(); });
|
||||
|
||||
try {
|
||||
await store.dispatch('payouts/expectations');
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(state.payouts.totalExpectations.undistributed).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('success fetches total expectations', async () => {
|
||||
jest.spyOn(payoutsService, 'expectations').mockReturnValue(
|
||||
Promise.resolve(expectations),
|
||||
);
|
||||
|
||||
await store.dispatch('payouts/expectations');
|
||||
|
||||
expect(state.payouts.totalExpectations.undistributed).toBe(expectations.undistributed);
|
||||
});
|
||||
|
||||
it('success fetches by node expectations', async () => {
|
||||
jest.spyOn(payoutsService, 'expectations').mockReturnValue(
|
||||
Promise.resolve(expectationsByNode),
|
||||
);
|
||||
|
||||
await store.dispatch('payouts/expectations', 'nodeId');
|
||||
|
||||
expect(state.payouts.totalExpectations.undistributed).toBe(0);
|
||||
expect(state.payouts.selectedNodeExpectations.undistributed).toBe(expectationsByNode.undistributed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getters', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user