diff --git a/web/multinode/src/api/index.ts b/web/multinode/src/api/index.ts index 1ca96e81d..c93d60c29 100644 --- a/web/multinode/src/api/index.ts +++ b/web/multinode/src/api/index.ts @@ -1,6 +1,8 @@ // Copyright (C) 2021 Storj Labs, Inc. // See LICENSE for copying information. +import { HttpClient } from '@/private/http/client'; + /** * ErrorUnauthorized is a custom error type for performing unauthorized operations. */ @@ -27,3 +29,37 @@ export class InternalError extends Error { super(message); } } + +/** + * APIClient is base client that holds http client and error handler. + */ +export class APIClient { + protected readonly http: HttpClient = new HttpClient(); + + /** + * handles error due to response code. + * @param response - response from server. + * + * @throws {@link BadRequestError} + * This exception is thrown if the input is not a valid ISBN number. + * + * @throws {@link UnauthorizedError} + * Thrown if the ISBN number is valid, but no such book exists in the catalog. + * + * @throws {@link InternalError} + * Thrown if the ISBN number is valid, but no such book exists in the catalog. + * + * @private + */ + protected async handleError(response: Response): Promise { + const body = await response.json(); + + switch (response.status) { + case 401: throw new UnauthorizedError(body.error); + case 400: throw new BadRequestError(body.error); + case 500: + default: + throw new InternalError(body.error); + } + } +} diff --git a/web/multinode/src/api/nodes.ts b/web/multinode/src/api/nodes.ts index 82e84c4fa..66befa5b9 100644 --- a/web/multinode/src/api/nodes.ts +++ b/web/multinode/src/api/nodes.ts @@ -1,15 +1,13 @@ // Copyright (C) 2021 Storj Labs, Inc. // See LICENSE for copying information. -import { BadRequestError, InternalError, UnauthorizedError } from '@/api/index'; +import { APIClient } from '@/api/index'; import { CreateNodeFields, Node, NodeURL } from '@/nodes'; -import { HttpClient } from '@/private/http/client'; /** * client for nodes controller of MND api. */ -export class NodesClient { - private readonly http: HttpClient = new HttpClient(); +export class NodesClient extends APIClient { private readonly ROOT_PATH: string = '/api/v0/nodes'; /** @@ -176,31 +174,4 @@ export class NodesClient { url.Name, )); } - - /** - * handles error due to response code. - * @param response - response from server. - * - * @throws {@link BadRequestError} - * This exception is thrown if the input is not a valid ISBN number. - * - * @throws {@link UnauthorizedError} - * Thrown if the ISBN number is valid, but no such book exists in the catalog. - * - * @throws {@link InternalError} - * Thrown if the ISBN number is valid, but no such book exists in the catalog. - * - * @private - */ - private async handleError(response: Response): Promise { - const body = await response.json(); - - switch (response.status) { - case 401: throw new UnauthorizedError(body.error); - case 400: throw new BadRequestError(body.error); - case 500: - default: - throw new InternalError(body.error); - } - } } diff --git a/web/multinode/src/api/payouts.ts b/web/multinode/src/api/payouts.ts new file mode 100644 index 000000000..c9da8b418 --- /dev/null +++ b/web/multinode/src/api/payouts.ts @@ -0,0 +1,38 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +import { APIClient } from '@/api/index'; +import { PayoutsSummary } from '@/payouts'; + +/** + * client for nodes controller of MND api. + */ +export class PayoutsClient extends APIClient { + private readonly ROOT_PATH: string = '/api/v0/payouts'; + + /** + * handles fetch of payouts summary information. + * + * @param satelliteId - satellite id. + * @param period - selected period. + * + * @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 summary(satelliteId: string | null, period: string | null): Promise { + const path = `${this.ROOT_PATH}/summary`; + const response = await this.http.get(path); + + if (!response.ok) { + await this.handleError(response); + } + + return await response.json(); + } +} diff --git a/web/multinode/src/app/store/index.ts b/web/multinode/src/app/store/index.ts index 3ad974ab5..b64a86ef6 100644 --- a/web/multinode/src/app/store/index.ts +++ b/web/multinode/src/app/store/index.ts @@ -5,8 +5,11 @@ import Vue from 'vue'; import Vuex, { ModuleTree, Store, StoreOptions } from 'vuex'; import { NodesClient } from '@/api/nodes'; +import { PayoutsClient } from '@/api/payouts'; import { NodesModule, NodesState } from '@/app/store/nodes'; +import { PayoutsModule, PayoutsState } from '@/app/store/payouts'; import { Nodes } from '@/nodes/service'; +import { Payouts } from '@/payouts/service'; Vue.use(Vuex); @@ -15,6 +18,7 @@ Vue.use(Vuex); */ export class RootState { nodes: NodesState; + payouts: PayoutsState; } /** @@ -25,13 +29,15 @@ export class MultinodeStoreOptions implements StoreOptions { public readonly state: RootState; public readonly modules: ModuleTree; - public constructor(nodes: NodesModule) { + public constructor(nodes: NodesModule, payouts: PayoutsModule) { this.strict = true; this.state = { nodes: nodes.state, + payouts: payouts.state, }; this.modules = { nodes, + payouts, }; } } @@ -39,11 +45,14 @@ export class MultinodeStoreOptions implements StoreOptions { // Services const nodesClient: NodesClient = new NodesClient(); const nodesService: Nodes = new Nodes(nodesClient); +const payoutsClient: PayoutsClient = new PayoutsClient(); +const payoutsService: Payouts = new Payouts(payoutsClient); // Modules const nodesModule: NodesModule = new NodesModule(nodesService); +const payoutsModule: PayoutsModule = new PayoutsModule(payoutsService); // Store export const store: Store = new Vuex.Store( - new MultinodeStoreOptions(nodesModule), + new MultinodeStoreOptions(nodesModule, payoutsModule), ); diff --git a/web/multinode/src/app/store/payouts.ts b/web/multinode/src/app/store/payouts.ts new file mode 100644 index 000000000..7ecc233e1 --- /dev/null +++ b/web/multinode/src/app/store/payouts.ts @@ -0,0 +1,60 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +import { ActionContext, ActionTree, GetterTree, Module, MutationTree } from 'vuex'; + +import { RootState } from '@/app/store/index'; +import { PayoutsSummary } from '@/payouts'; +import { Payouts } from '@/payouts/service'; + +/** + * PayoutsState is a representation of payouts module state. + */ +export class PayoutsState { + public summary: PayoutsSummary; +} + +/** + * NodesModule is a part of a global store that encapsulates all nodes related logic. + */ +export class PayoutsModule implements Module { + public readonly namespaced: boolean; + public readonly state: PayoutsState; + public readonly getters?: GetterTree; + public readonly actions: ActionTree; + public readonly mutations: MutationTree; + + private readonly payouts: any; + + public constructor(payouts: Payouts) { + this.payouts = payouts; + + this.namespaced = true; + this.state = new PayoutsState(); + this.mutations = { + populate: this.populate, + }; + this.actions = { + getSummary: this.getSummary.bind(this), + }; + } + + /** + * populate mutation will set payouts state. + * @param state - state of the module. + * @param summary - payouts summary information depends on selected time and satellite. + */ + public populate(state: PayoutsState, summary: PayoutsSummary): void { + state.summary = summary; + } + + /** + * getSummary action loads payouts summary information. + * @param ctx - context of the Vuex action. + */ + public async getSummary(ctx: ActionContext): Promise { + // @ts-ignore + const summary = await this.payouts.summary(ctx.rootState.nodes.selectedSatellite.id, ''); + ctx.commit('populate', summary); + } +} diff --git a/web/multinode/src/payouts/index.ts b/web/multinode/src/payouts/index.ts new file mode 100644 index 000000000..26cb0664f --- /dev/null +++ b/web/multinode/src/payouts/index.ts @@ -0,0 +1,50 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +/** + * PayoutsSummary is a representation of summary of payout information for node. + */ +export class NodePayoutsSummary { + public constructor( + public nodeID: string = '', + public nodeName: string = '', + public held: number = 0, + public paid: number = 0, + ) {} +} + +/** + * PayoutsSummary is a representation of summary of payout information for all selected nodes and list of payout summaries by nodes. + */ +export class PayoutsSummary { + public constructor( + public totalEarned: number = 0, + public totalHeld: number = 0, + public totalPaid: number = 0, + public nodeSummary: NodePayoutsSummary[] = [], + ) {} +} + +/** + * Represents payout period month and year. + */ +export class PayoutPeriod { + public constructor( + public year: number = new Date().getUTCFullYear(), + public month: number = new Date().getUTCMonth(), + ) {} + + public get period(): string { + return this.month < 9 ? `${this.year}-0${this.month + 1}` : `${this.year}-${this.month + 1}`; + } + + /** + * Parses PayoutPeriod from string. + * @param period string + */ + public static fromString(period: string): PayoutPeriod { + const periodArray = period.split('-'); + + return new PayoutPeriod(parseInt(periodArray[0]), parseInt(periodArray[1]) - 1); + } +} diff --git a/web/multinode/src/payouts/service.ts b/web/multinode/src/payouts/service.ts new file mode 100644 index 000000000..98c09cbcb --- /dev/null +++ b/web/multinode/src/payouts/service.ts @@ -0,0 +1,47 @@ +// Copyright (C) 2021 Storj Labs, Inc. +// See LICENSE for copying information. + +import { PayoutsClient } from '@/api/payouts'; +import { NodePayoutsSummary, PayoutsSummary } from '@/payouts/index'; + +/** + * exposes all payouts related logic + */ +export class Payouts { + private readonly payouts: PayoutsClient; + + public constructor(payouts: PayoutsClient) { + this.payouts = payouts; + } + + /** + * fetches of payouts summary information. + * + * @param satelliteId - satellite id. + * @param period - selected period. + * + * @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 summary(satelliteId: string | null, period: string | null): Promise { + const result = await this.payouts.summary(satelliteId, period); + + return new PayoutsSummary( + result.totalEarned, + result.totalHeld, + result.totalPaid, + result.nodeSummary.map(item => new NodePayoutsSummary( + item.nodeID, + item.nodeName, + item.held, + item.paid, + )), + ); + } +}