web/multinode: payouts page markup
WHAT: total payouts table, period calendar, details area and payout history area markup also this component fumctionally connected to api and vuex store Change-Id: Id3abc87bc7545aa3fc0a7ef2e480a8ed73974b76
This commit is contained in:
parent
6ee2210297
commit
2ccbb32e14
@ -26,7 +26,8 @@ export class PayoutsClient extends APIClient {
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async summary(satelliteId: string | null, period: string | null): Promise<PayoutsSummary> {
|
||||
const path = `${this.ROOT_PATH}/summary`;
|
||||
const path = `${this.ROOT_PATH}/summary?satelliteId=${satelliteId}&period=${period}`;
|
||||
|
||||
const response = await this.http.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
|
26
web/multinode/src/app/components/common/InfoBlock.vue
Normal file
26
web/multinode/src/app/components/common/InfoBlock.vue
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="info-block">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class InfoBlock extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.info-block {
|
||||
box-sizing: border-box;
|
||||
padding: 30px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--c-gray--light);
|
||||
border-radius: var(--br-block);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
101
web/multinode/src/app/components/common/NodeOptions.vue
Normal file
101
web/multinode/src/app/components/common/NodeOptions.vue
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="options-button" @click.stop="toggleOptions">
|
||||
<more-icon />
|
||||
<div class="options" v-if="areOptionsShown" v-click-outside="closeOptions">
|
||||
<div @click.stop="onCopy" class="options__item">Copy Node ID</div>
|
||||
<delete-node :node-id="id" />
|
||||
<update-name :node-id="id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import DeleteNode from '@/app/components/modals/DeleteNode.vue';
|
||||
import UpdateName from '@/app/components/modals/UpdateName.vue';
|
||||
|
||||
import MoreIcon from '@/../static/images/icons/more.svg';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
UpdateName,
|
||||
DeleteNode,
|
||||
MoreIcon,
|
||||
},
|
||||
})
|
||||
export default class NodeItem extends Vue {
|
||||
@Prop({default: ''})
|
||||
public id: string;
|
||||
|
||||
public areOptionsShown: boolean = false;
|
||||
|
||||
public toggleOptions(): void {
|
||||
this.areOptionsShown = !this.areOptionsShown;
|
||||
}
|
||||
|
||||
public closeOptions(): void {
|
||||
if (!this.areOptionsShown) return;
|
||||
|
||||
this.areOptionsShown = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies node id to clipboard and closes popup.
|
||||
*/
|
||||
public async onCopy(): Promise<void> {
|
||||
try {
|
||||
await this.$copyText(this.id);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
|
||||
this.closeOptions();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.options-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: var(--c-background);
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 55px;
|
||||
width: 140px;
|
||||
height: auto;
|
||||
background: white;
|
||||
border-radius: var(--br-table);
|
||||
font-family: 'font_medium', sans-serif;
|
||||
border: 1px solid var(--c-gray--light);
|
||||
font-size: 14px;
|
||||
color: var(--c-title);
|
||||
z-index: 999;
|
||||
|
||||
&__item {
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: var(--c-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<v-dropdown :options="trustedSatellitesOptions" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import VDropdown, { Option } from '@/app/components/common/VDropdown.vue';
|
||||
import NodesTable from '@/app/components/myNodes/tables/NodesTable.vue';
|
||||
|
||||
import { NodeURL } from '@/nodes';
|
||||
|
||||
@Component({
|
||||
components: { VDropdown, NodesTable },
|
||||
})
|
||||
export default class SatelliteSelectionDropdown extends Vue {
|
||||
/**
|
||||
* List of trusted satellites and all satellites options.
|
||||
*/
|
||||
public get trustedSatellitesOptions(): Option[] {
|
||||
const trustedSatellites: NodeURL[] = this.$store.state.nodes.trustedSatellites;
|
||||
|
||||
const options: Option[] = trustedSatellites.map(
|
||||
(satellite: NodeURL) => {
|
||||
return new Option(satellite.id, () => this.onSatelliteClick(satellite.id));
|
||||
},
|
||||
);
|
||||
|
||||
return [ new Option('All Satellites', () => this.onSatelliteClick()), ...options ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for satellite click.
|
||||
* @param id
|
||||
*/
|
||||
public async onSatelliteClick(id: string = ''): Promise<void> {
|
||||
await this.$store.dispatch('nodes/selectSatellite', id);
|
||||
}
|
||||
}
|
||||
</script>
|
@ -9,6 +9,9 @@
|
||||
v-if="options.length"
|
||||
>
|
||||
<span class="label">{{ selectedOption.label }}</span>
|
||||
<svg width="8" height="4" viewBox="0 0 8 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33657 3.73107C3.70296 4.09114 4.29941 4.08814 4.66237 3.73107L7.79796 0.650836C8.16435 0.291517 8.01864 0 7.47247 0L0.526407 0C-0.0197628 0 -0.16292 0.294525 0.200917 0.650836L3.33657 3.73107Z" fill="#131D3A"/>
|
||||
</svg>
|
||||
<div class="dropdown__selection" v-if="areOptionsShown" v-click-outside="closeOptions">
|
||||
<div class="dropdown__selection__overflow-container">
|
||||
<div v-for="option in options" :key="option.label" class="dropdown__selection__option" @click="onOptionClick(option)">
|
||||
@ -77,7 +80,8 @@ export default class VDropdown extends Vue {
|
||||
.dropdown {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 300px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 40px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
@ -114,7 +118,7 @@ export default class VDropdown extends Vue {
|
||||
&__overflow-container {
|
||||
overflow: overlay;
|
||||
overflow-x: hidden;
|
||||
height: 160px;
|
||||
max-height: 160px;
|
||||
}
|
||||
|
||||
&__option {
|
||||
|
87
web/multinode/src/app/components/myNodes/tables/NodeItem.vue
Normal file
87
web/multinode/src/app/components/myNodes/tables/NodeItem.vue
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<tr class="node-item">
|
||||
<th class="align-left">{{ node.displayedName }}</th>
|
||||
<template v-if="isSatelliteSelected">
|
||||
<th>{{ node.suspensionScore | floatToPercentage }}</th>
|
||||
<th>{{ node.auditScore | floatToPercentage }}</th>
|
||||
<th>{{ node.onlineScore | floatToPercentage }}</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th>{{ node.diskSpaceUsed | bytesToBase10String }}</th>
|
||||
<th>{{ node.diskSpaceLeft | bytesToBase10String }}</th>
|
||||
<th>{{ node.bandwidthUsed | bytesToBase10String }}</th>
|
||||
</template>
|
||||
<th>{{ node.earned | centsToDollars }}</th>
|
||||
<th>{{ node.version }}</th>
|
||||
<th :class="node.status">{{ node.status }}</th>
|
||||
<th class="overflow-visible">
|
||||
<node-options :id="node.id" />
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import NodeOptions from '@/app/components/common/NodeOptions.vue';
|
||||
|
||||
import { Node } from '@/nodes';
|
||||
|
||||
@Component({
|
||||
components: { NodeOptions },
|
||||
})
|
||||
export default class NodeItem extends Vue {
|
||||
@Prop({default: () => new Node()})
|
||||
public node: Node;
|
||||
|
||||
public get isSatelliteSelected(): boolean {
|
||||
return !!this.$store.state.nodes.selectedSatellite;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-item {
|
||||
height: 56px;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
color: var(--c-line);
|
||||
|
||||
th {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:nth-of-type(even) {
|
||||
background: var(--c-block-gray);
|
||||
}
|
||||
|
||||
th:not(:first-of-type) {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.online {
|
||||
color: var(--c-success);
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: var(--c-error);
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.overflow-visible {
|
||||
overflow: visible !important;
|
||||
}
|
||||
</style>
|
@ -31,7 +31,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import NodeItem from '@/app/components/tables/NodeItem.vue';
|
||||
import NodeItem from '@/app/components/myNodes/tables/NodeItem.vue';
|
||||
|
||||
import { Node } from '@/nodes';
|
||||
|
104
web/multinode/src/app/components/payouts/DetailsArea.vue
Normal file
104
web/multinode/src/app/components/payouts/DetailsArea.vue
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<info-block>
|
||||
<div class="details-area" slot="body">
|
||||
<header class="details-area__header">
|
||||
<h3 class="details-area__header__title">Details</h3>
|
||||
<p class="details-area__header__period">{{ period }}</p>
|
||||
</header>
|
||||
<div class="details-area__content">
|
||||
<div class="details-area__content__item">
|
||||
<p class="details-area__content__item__label">EARNED</p>
|
||||
<p class="details-area__content__item__value">{{ totalEarned | centsToDollars }}</p>
|
||||
</div>
|
||||
<div class="details-area__content__item">
|
||||
<p class="details-area__content__item__label">HELD</p>
|
||||
<p class="details-area__content__item__value">{{ totalHeld | centsToDollars }}</p>
|
||||
</div>
|
||||
<div class="details-area__content__item">
|
||||
<p class="details-area__content__item__label">PAID</p>
|
||||
<p class="details-area__content__item__value">{{ totalPaid | 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 DetailsArea extends Vue {
|
||||
@Prop({default: 0})
|
||||
public totalEarned: number;
|
||||
@Prop({default: 0})
|
||||
public totalHeld: number;
|
||||
@Prop({default: 0})
|
||||
public totalPaid: number;
|
||||
|
||||
public get period(): string {
|
||||
return this.$store.getters['payouts/periodString'];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.details-area {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: var(--c-title);
|
||||
}
|
||||
|
||||
&__period {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 16px;
|
||||
color: var(--c-gray);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
font-family: 'font_semiBold', sans-serif;
|
||||
|
||||
&:last-of-type {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
color: var(--c-gray);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 18px;
|
||||
color: var(--c-title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<info-block>
|
||||
<div class="payouts-history-block" slot="body">
|
||||
<h3 class="payouts-history-block__title">Payout History</h3>
|
||||
<v-button class="payouts-history-block__button" label="Download" :is-white="true" width="100%" :on-press="() => {}" />
|
||||
</div>
|
||||
</info-block>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import InfoBlock from '@/app/components/common/InfoBlock.vue';
|
||||
import VButton from '@/app/components/common/VButton.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
InfoBlock,
|
||||
VButton,
|
||||
},
|
||||
})
|
||||
export default class PayoutHistoryBlock extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payouts-history-block {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 24px;
|
||||
color: var(--c-title);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
background-color: var(--c-button-gray);
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,338 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="payout-period-calendar">
|
||||
<div class="payout-period-calendar__header">
|
||||
<div class="payout-period-calendar__header__year-selection">
|
||||
<div class="payout-period-calendar__header__year-selection__prev" @click="decrementYear">
|
||||
<GrayArrowLeftIcon />
|
||||
</div>
|
||||
<p class="payout-period-calendar__header__year-selection__year">{{ displayedYear }}</p>
|
||||
<div class="payout-period-calendar__header__year-selection__next" @click="incrementYear">
|
||||
<GrayArrowLeftIcon />
|
||||
</div>
|
||||
</div>
|
||||
<p class="payout-period-calendar__header__all-time" @click="selectAllTime" >All time</p>
|
||||
</div>
|
||||
<div class="payout-period-calendar__months-area">
|
||||
<div
|
||||
class="month-item"
|
||||
:class="{ selected: item.selected, disabled: !item.active }"
|
||||
v-for="item in currentDisplayedMonths"
|
||||
:key="item.name"
|
||||
@click="checkMonth(item)"
|
||||
>
|
||||
<p class="month-item__label">{{ item.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payout-period-calendar__footer-area">
|
||||
<p class="payout-period-calendar__footer-area__period">{{ period }}</p>
|
||||
<v-button label="Done" :is-disabled="!period" width="73px" height="40px" :on-press="submit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import VButton from '@/app/components/common/VButton.vue';
|
||||
|
||||
import GrayArrowLeftIcon from '@/../static/images/icons/GrayArrowLeft.svg';
|
||||
|
||||
import { UnauthorizedError } from '@/api';
|
||||
import { monthNames } from '@/app/types/date';
|
||||
import { MonthButton, StoredMonthsByYear } from '@/app/types/payouts';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
VButton,
|
||||
GrayArrowLeftIcon,
|
||||
},
|
||||
})
|
||||
export default class PayoutHistoryPeriodCalendar extends Vue {
|
||||
private now: Date = new Date();
|
||||
/**
|
||||
* Contains current months list depends on active and selected month state.
|
||||
*/
|
||||
public currentDisplayedMonths: MonthButton[] = [];
|
||||
public displayedYear: number = this.now.getUTCFullYear();
|
||||
public period: string = '';
|
||||
|
||||
private displayedMonths: StoredMonthsByYear = {};
|
||||
private selectedMonth: MonthButton | null;
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Sets up current calendar state.
|
||||
*/
|
||||
public mounted(): void {
|
||||
this.populateMonths(this.displayedYear);
|
||||
this.currentDisplayedMonths = this.displayedMonths[this.displayedYear];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches payout information.
|
||||
*/
|
||||
public async submit(): Promise<void> {
|
||||
let period: string | null = null;
|
||||
if (this.selectedMonth) {
|
||||
const month = this.selectedMonth.index < 9 ? '0' + (this.selectedMonth.index + 1) : (this.selectedMonth.index + 1);
|
||||
period = `${this.selectedMonth.year}-${month}`;
|
||||
}
|
||||
|
||||
this.$store.commit('payouts/setPayoutPeriod', period);
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('payouts/summary');
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// TODO: redirect to login screen.
|
||||
}
|
||||
|
||||
// TODO: notify error
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates selected period label.
|
||||
*/
|
||||
public updatePeriod(): void {
|
||||
if (!this.selectedMonth) {
|
||||
this.period = 'All time';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.period = `${monthNames[this.selectedMonth.index]}, ${this.selectedMonth.year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates first selected month on click.
|
||||
*/
|
||||
public checkMonth(month: MonthButton): void {
|
||||
if (!month.active || month.selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedMonth) {
|
||||
this.selectedMonth.selected = false;
|
||||
}
|
||||
|
||||
this.selectedMonth = month;
|
||||
month.selected = true;
|
||||
this.updatePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* selectAllTime resets selected payout period.
|
||||
*/
|
||||
public selectAllTime(): void {
|
||||
if (this.selectedMonth) {
|
||||
this.selectedMonth.selected = false;
|
||||
}
|
||||
|
||||
this.selectedMonth = null;
|
||||
this.updatePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments year and updates current months set.
|
||||
*/
|
||||
public incrementYear(): void {
|
||||
const isCurrentYear = this.displayedYear === this.now.getUTCFullYear();
|
||||
|
||||
if (isCurrentYear) return;
|
||||
|
||||
this.displayedYear += 1;
|
||||
this.populateMonths(this.displayedYear);
|
||||
this.currentDisplayedMonths = this.displayedMonths[this.displayedYear];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement year and updates current months set.
|
||||
*/
|
||||
public decrementYear(): void {
|
||||
// TODO: remove hardcoded value
|
||||
const minYear: number = 2000;
|
||||
|
||||
if (this.displayedYear === minYear) return;
|
||||
|
||||
this.displayedYear -= 1;
|
||||
this.populateMonths(this.displayedYear);
|
||||
this.currentDisplayedMonths = this.displayedMonths[this.displayedYear];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets months set in displayedMonths with year as key.
|
||||
*/
|
||||
private populateMonths(year: number): void {
|
||||
if (this.displayedMonths[year]) {
|
||||
this.currentDisplayedMonths = this.displayedMonths[year];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const months: MonthButton[] = [];
|
||||
|
||||
// Creates months entities and adds them to list.
|
||||
for (let i = 0; i < 12; i++) {
|
||||
months.push(new MonthButton(year, i, true, false));
|
||||
}
|
||||
|
||||
this.displayedMonths[year] = months;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes calendar.
|
||||
*/
|
||||
private close(): void {
|
||||
this.$emit('onClose');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payout-period-calendar {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 333px;
|
||||
height: 340px;
|
||||
background: white;
|
||||
box-shadow: 0 10px 25px rgba(175, 183, 193, 0.1);
|
||||
border-radius: var(--br-block);
|
||||
border: 1px solid #e1e3e6;
|
||||
padding: 30px;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
cursor: default;
|
||||
z-index: 1001;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&__year-selection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
&__prev,
|
||||
&__next {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&__prev {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&__year {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
color: var(--c-title);
|
||||
}
|
||||
|
||||
&__next {
|
||||
transform: rotate(180deg);
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__all-time {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 16px;
|
||||
color: var(--c-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__months-area {
|
||||
display: grid;
|
||||
grid-template-columns: 93px 93px 93px;
|
||||
grid-gap: 1px;
|
||||
background: var(--c-gray--light);
|
||||
overflow: hidden;
|
||||
border-radius: var(--br-table);
|
||||
border: 1px solid var(--c-gray--light);
|
||||
}
|
||||
|
||||
&__footer-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
margin-top: 7px;
|
||||
|
||||
&__period {
|
||||
font-family: 'font_semiBold', sans-serif;
|
||||
font-size: 16px;
|
||||
color: var(--c-payout-period);
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
&__ok-button {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
color: var(--c-button-common);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.month-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
|
||||
&__label {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
color: var(--c-title);
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background: var(--c-button-disabled);
|
||||
cursor: default;
|
||||
|
||||
.month-item__label {
|
||||
color: var(--c-gray) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: var(--c-primary);
|
||||
|
||||
.month-item__label {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
|
||||
path {
|
||||
fill: var(--c-gray);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,82 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="calendar-button"
|
||||
@click.stop="openCalendar"
|
||||
>
|
||||
<span class="label">{{ period }}</span>
|
||||
<svg width="8" height="4" viewBox="0 0 8 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33657 3.73107C3.70296 4.09114 4.29941 4.08814 4.66237 3.73107L7.79796 0.650836C8.16435 0.291517 8.01864 0 7.47247 0L0.526407 0C-0.0197628 0 -0.16292 0.294525 0.200917 0.650836L3.33657 3.73107Z" fill="#131D3A"/>
|
||||
</svg>
|
||||
<payout-period-calendar class="calendar-button__calendar" v-if="isCalendarShown" @onClose="closeCalendar" v-click-outside="close" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import PayoutPeriodCalendar from './PayoutPeriodCalendar.vue';
|
||||
|
||||
@Component({
|
||||
components: { PayoutPeriodCalendar },
|
||||
})
|
||||
export default class Payout extends Vue {
|
||||
public isCalendarShown: boolean = false;
|
||||
|
||||
public get period(): string {
|
||||
return this.$store.getters['payouts/periodString'];
|
||||
}
|
||||
|
||||
public openCalendar(): void {
|
||||
this.isCalendarShown = true;
|
||||
}
|
||||
|
||||
public closeCalendar(): void {
|
||||
if (!this.isCalendarShown) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.isCalendarShown = false;
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.calendar-button {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 40px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--c-gray--light);
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
color: var(--c-title);
|
||||
cursor: pointer;
|
||||
font-family: 'font_medium', sans-serif;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--c-gray);
|
||||
color: var(--c-title);
|
||||
}
|
||||
|
||||
&__calendar {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,74 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<tr class="payouts-summary-item">
|
||||
<th class="align-left node-name">{{ payoutsSummary.nodeName }}</th>
|
||||
<th>{{ payoutsSummary.held | centsToDollars }}</th>
|
||||
<th>{{ payoutsSummary.paid | centsToDollars }}</th>
|
||||
<th class="overflow-visible options">
|
||||
<node-options :id="payoutsSummary.nodeID" />
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import NodeOptions from '@/app/components/common/NodeOptions.vue';
|
||||
|
||||
import { NodePayoutsSummary } from '@/payouts';
|
||||
|
||||
@Component({
|
||||
components: { NodeOptions },
|
||||
})
|
||||
export default class PayoutsSummaryItem extends Vue {
|
||||
@Prop({default: () => new NodePayoutsSummary()})
|
||||
public payoutsSummary: NodePayoutsSummary;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payouts-summary-item {
|
||||
height: 56px;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
color: var(--c-line);
|
||||
cursor: pointer;
|
||||
|
||||
th {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:nth-of-type(even) {
|
||||
background: var(--c-block-gray);
|
||||
}
|
||||
|
||||
th:not(:first-of-type) {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
color: var(--c-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.overflow-visible {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,77 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<table class="payouts-summary-table" v-if="nodePayoutsSummary.length" border="0" cellpadding="0" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="align-left">NODE</th>
|
||||
<th>HELD</th>
|
||||
<th>PAID</th>
|
||||
<th class="options"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<payouts-summary-item v-for="payoutSummary in nodePayoutsSummary" :key="payoutSummary.nodeID" :payouts-summary="payoutSummary"/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import NodeItem from '@/app/components/myNodes/tables/NodeItem.vue';
|
||||
import PayoutsSummaryItem from '@/app/components/payouts/tables/PayoutsSummaryItem.vue';
|
||||
|
||||
import { NodePayoutsSummary } from '@/payouts';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
PayoutsSummaryItem,
|
||||
NodeItem,
|
||||
},
|
||||
})
|
||||
export default class PayoutsSummaryTable extends Vue {
|
||||
@Prop({default: () => []})
|
||||
public nodePayoutsSummary: NodePayoutsSummary[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payouts-summary-table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--c-gray--light);
|
||||
border-radius: var(--br-table);
|
||||
font-family: 'font_semiBold', sans-serif;
|
||||
z-index: 999;
|
||||
|
||||
th {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--c-block-gray);
|
||||
|
||||
tr {
|
||||
height: 40px;
|
||||
font-size: 12px;
|
||||
color: var(--c-gray);
|
||||
border-radius: var(--br-table);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,163 +0,0 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<tr class="node-item">
|
||||
<th class="align-left">{{ node.displayedName }}</th>
|
||||
<template v-if="isSatelliteSelected">
|
||||
<th>{{ node.suspensionScore | floatToPercentage }}</th>
|
||||
<th>{{ node.auditScore | floatToPercentage }}</th>
|
||||
<th>{{ node.onlineScore | floatToPercentage }}</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th>{{ node.diskSpaceUsed | bytesToBase10String }}</th>
|
||||
<th>{{ node.diskSpaceLeft | bytesToBase10String }}</th>
|
||||
<th>{{ node.bandwidthUsed | bytesToBase10String }}</th>
|
||||
</template>
|
||||
<th>{{ node.earned | centsToDollars }}</th>
|
||||
<th>{{ node.version }}</th>
|
||||
<th :class="node.status">{{ node.status }}</th>
|
||||
<th class="overflow-visible">
|
||||
<div class="node-item__options-button" @click.stop="toggleOptions">
|
||||
<more-icon />
|
||||
</div>
|
||||
<div class="node-item__options" v-if="areOptionsShown" v-click-outside="closeOptions">
|
||||
<div @click.stop="() => onCopy(node.id)" class="node-item__options__item">Copy Node ID</div>
|
||||
<delete-node :node-id="node.id" />
|
||||
<update-name :node-id="node.id" />
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import DeleteNode from '@/app/components/modals/DeleteNode.vue';
|
||||
import UpdateName from '@/app/components/modals/UpdateName.vue';
|
||||
|
||||
import MoreIcon from '@/../static/images/icons/more.svg';
|
||||
|
||||
import { Node } from '@/nodes';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
UpdateName,
|
||||
DeleteNode,
|
||||
MoreIcon,
|
||||
},
|
||||
})
|
||||
export default class NodeItem extends Vue {
|
||||
@Prop({default: () => new Node()})
|
||||
public node: Node;
|
||||
|
||||
public areOptionsShown: boolean = false;
|
||||
|
||||
public get isSatelliteSelected(): boolean {
|
||||
return !!this.$store.state.nodes.selectedSatellite;
|
||||
}
|
||||
|
||||
public toggleOptions(): void {
|
||||
this.areOptionsShown = !this.areOptionsShown;
|
||||
}
|
||||
|
||||
public closeOptions(): void {
|
||||
if (!this.areOptionsShown) return;
|
||||
|
||||
this.areOptionsShown = false;
|
||||
}
|
||||
|
||||
public async onCopy(id: string): Promise<void> {
|
||||
try {
|
||||
await this.$copyText(id);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
|
||||
this.closeOptions();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-item {
|
||||
height: 56px;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
color: var(--c-line);
|
||||
|
||||
th {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:nth-of-type(even) {
|
||||
background: var(--c-block-gray);
|
||||
}
|
||||
|
||||
th:not(:first-of-type) {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
}
|
||||
|
||||
&__options-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: var(--c-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__options {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 55px;
|
||||
width: 140px;
|
||||
height: auto;
|
||||
background: white;
|
||||
border-radius: var(--br-table);
|
||||
font-family: 'font_medium', sans-serif;
|
||||
border: 1px solid var(--c-gray--light);
|
||||
font-size: 14px;
|
||||
color: var(--c-title);
|
||||
z-index: 999;
|
||||
|
||||
&__item {
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: var(--c-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.online {
|
||||
color: var(--c-success);
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: var(--c-error);
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.overflow-visible {
|
||||
overflow: visible !important;
|
||||
}
|
||||
</style>
|
@ -7,6 +7,7 @@ import { Component } from 'vue-router/types/router';
|
||||
import AddFirstNode from '@/app/views/AddFirstNode.vue';
|
||||
import Dashboard from '@/app/views/Dashboard.vue';
|
||||
import MyNodes from '@/app/views/MyNodes.vue';
|
||||
import PayoutsPage from '@/app/views/PayoutsPage.vue';
|
||||
import WelcomeScreen from '@/app/views/WelcomeScreen.vue';
|
||||
|
||||
/**
|
||||
@ -59,11 +60,13 @@ export class Config {
|
||||
public static Welcome: Route = new Route('/welcome', 'Welcome', WelcomeScreen);
|
||||
public static AddFirstNode: Route = new Route('/add-first-node', 'AddFirstNode', AddFirstNode);
|
||||
public static MyNodes: Route = new Route('/my-nodes', 'MyNodes', MyNodes);
|
||||
public static Payouts: Route = new Route('/payouts', 'Payouts', PayoutsPage);
|
||||
|
||||
public static mode: RouterMode = 'history';
|
||||
public static routes: Route[] = [
|
||||
Config.Root.addChildren([
|
||||
Config.MyNodes,
|
||||
Config.Payouts,
|
||||
]),
|
||||
Config.Welcome,
|
||||
Config.AddFirstNode,
|
||||
|
@ -4,6 +4,7 @@
|
||||
import { ActionContext, ActionTree, GetterTree, Module, MutationTree } from 'vuex';
|
||||
|
||||
import { RootState } from '@/app/store/index';
|
||||
import { monthNames } from '@/app/types/date';
|
||||
import { PayoutsSummary } from '@/payouts';
|
||||
import { Payouts } from '@/payouts/service';
|
||||
|
||||
@ -11,11 +12,12 @@ import { Payouts } from '@/payouts/service';
|
||||
* PayoutsState is a representation of payouts module state.
|
||||
*/
|
||||
export class PayoutsState {
|
||||
public summary: PayoutsSummary;
|
||||
public summary: PayoutsSummary = new PayoutsSummary();
|
||||
public selectedPayoutPeriod: string | null = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* NodesModule is a part of a global store that encapsulates all nodes related logic.
|
||||
* PayoutsModule is a part of a global store that encapsulates all payouts related logic.
|
||||
*/
|
||||
export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
public readonly namespaced: boolean;
|
||||
@ -33,12 +35,17 @@ export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
this.state = new PayoutsState();
|
||||
this.mutations = {
|
||||
populate: this.populate,
|
||||
setPayoutPeriod: this.setPayoutPeriod,
|
||||
};
|
||||
this.actions = {
|
||||
getSummary: this.getSummary.bind(this),
|
||||
summary: this.summary.bind(this),
|
||||
};
|
||||
this.getters = {
|
||||
periodString: this.periodString,
|
||||
};
|
||||
}
|
||||
|
||||
// Mutations
|
||||
/**
|
||||
* populate mutation will set payouts state.
|
||||
* @param state - state of the module.
|
||||
@ -49,12 +56,37 @@ export class PayoutsModule implements Module<PayoutsState, RootState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* getSummary action loads payouts summary information.
|
||||
* setPayoutPeriod mutation will save selected period to store.
|
||||
* @param state
|
||||
* @param period representation of month and year
|
||||
*/
|
||||
public setPayoutPeriod(state: PayoutsState, period: string | null) {
|
||||
state.selectedPayoutPeriod = period;
|
||||
}
|
||||
|
||||
// Actions
|
||||
/**
|
||||
* summary action loads payouts summary information.
|
||||
* @param ctx - context of the Vuex action.
|
||||
*/
|
||||
public async getSummary(ctx: ActionContext<PayoutsState, RootState>): Promise<void> {
|
||||
public async summary(ctx: ActionContext<PayoutsState, RootState>): Promise<void> {
|
||||
// @ts-ignore
|
||||
const summary = await this.payouts.summary(ctx.rootState.nodes.selectedSatellite.id, '');
|
||||
const summary = await this.payouts.summary(ctx.rootState.nodes.selectedSatellite.id, ctx.state.selectedPayoutPeriod);
|
||||
|
||||
ctx.commit('populate', summary);
|
||||
}
|
||||
|
||||
// Getters
|
||||
/**
|
||||
* periodString is full name month and year representation of selected payout period.
|
||||
*/
|
||||
public periodString(state: PayoutsState): string {
|
||||
if (!state.selectedPayoutPeriod) return 'All time';
|
||||
|
||||
const splittedPeriod = state.selectedPayoutPeriod.split('-');
|
||||
const monthIndex = parseInt(splittedPeriod[1]) - 1;
|
||||
const year = splittedPeriod[0];
|
||||
|
||||
return `${monthNames[monthIndex]}, ${year}`;
|
||||
}
|
||||
}
|
||||
|
11
web/multinode/src/app/types/date.ts
Normal file
11
web/multinode/src/app/types/date.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* Holds all months names.
|
||||
*/
|
||||
export const monthNames = [
|
||||
'January', 'February', 'March', 'April',
|
||||
'May', 'June', 'July', 'August',
|
||||
'September', 'October', 'November', 'December',
|
||||
];
|
27
web/multinode/src/app/types/payouts.ts
Normal file
27
web/multinode/src/app/types/payouts.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { monthNames } from '@/app/types/date';
|
||||
|
||||
/**
|
||||
* Describes month button entity for calendar.
|
||||
*/
|
||||
export class MonthButton {
|
||||
public constructor(
|
||||
public year: number = 0,
|
||||
public index: number = 0,
|
||||
public active: boolean = false,
|
||||
public selected: boolean = false,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns month label depends on index.
|
||||
*/
|
||||
public get name(): string {
|
||||
return monthNames[this.index] ? monthNames[this.index].slice(0, 3) : '';
|
||||
}
|
||||
}
|
||||
|
||||
export interface StoredMonthsByYear {
|
||||
[key: number]: MonthButton[];
|
||||
}
|
@ -3,13 +3,13 @@
|
||||
|
||||
<template>
|
||||
<div class="dashboard-area">
|
||||
<div class="dashboard-area__navigation-area">
|
||||
<nav class="dashboard-area__navigation-area">
|
||||
<navigation-area />
|
||||
</div>
|
||||
</nav>
|
||||
<div class="dashboard-area__right-area">
|
||||
<div class="dashboard-area__right-area__header">
|
||||
<header class="dashboard-area__right-area__header">
|
||||
<add-new-node />
|
||||
</div>
|
||||
</header>
|
||||
<div class="dashboard-area__right-area__content">
|
||||
<router-view />
|
||||
</div>
|
||||
@ -23,13 +23,26 @@ import { Component, Vue } from 'vue-property-decorator';
|
||||
import AddNewNode from '@/app/components/modals/AddNewNode.vue';
|
||||
import NavigationArea from '@/app/components/navigation/NavigationArea.vue';
|
||||
|
||||
import { UnauthorizedError } from '@/api';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
AddNewNode,
|
||||
NavigationArea,
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {}
|
||||
export default class Dashboard extends Vue {
|
||||
public async mounted(): Promise<void> {
|
||||
try {
|
||||
await this.$store.dispatch('nodes/trustedSatellites');
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// TODO: redirect to login screen.
|
||||
}
|
||||
// TODO: notify error
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<template>
|
||||
<div class="my-nodes">
|
||||
<h1 class="my-nodes__title">My Nodes</h1>
|
||||
<v-dropdown :options="trustedSatellitesOptions" />
|
||||
<satellite-selection-dropdown />
|
||||
<nodes-table class="table"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -12,26 +12,19 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import VDropdown, { Option } from '@/app/components/common/VDropdown.vue';
|
||||
import NodesTable from '@/app/components/tables/NodesTable.vue';
|
||||
import SatelliteSelectionDropdown from '@/app/components/common/SatelliteSelectionDropdown.vue';
|
||||
import NodesTable from '@/app/components/myNodes/tables/NodesTable.vue';
|
||||
|
||||
import { UnauthorizedError } from '@/api';
|
||||
import { NodeURL } from '@/nodes';
|
||||
|
||||
@Component({
|
||||
components: { VDropdown, NodesTable },
|
||||
components: {
|
||||
SatelliteSelectionDropdown,
|
||||
NodesTable,
|
||||
},
|
||||
})
|
||||
export default class MyNodes extends Vue {
|
||||
public async mounted(): Promise<void> {
|
||||
try {
|
||||
await this.$store.dispatch('nodes/trustedSatellites');
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// TODO: redirect to login screen.
|
||||
}
|
||||
// TODO: notify error
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('nodes/fetch');
|
||||
} catch (error) {
|
||||
@ -42,22 +35,6 @@ export default class MyNodes extends Vue {
|
||||
// TODO: notify error
|
||||
}
|
||||
}
|
||||
|
||||
public get trustedSatellitesOptions(): Option[] {
|
||||
const trustedSatellites: NodeURL[] = this.$store.state.nodes.trustedSatellites;
|
||||
|
||||
const options: Option[] = trustedSatellites.map(
|
||||
(satellite: NodeURL) => {
|
||||
return new Option(satellite.id, () => this.onSatelliteClick(satellite.id));
|
||||
},
|
||||
);
|
||||
|
||||
return [ new Option('All Satellites', () => this.onSatelliteClick()), ...options ];
|
||||
}
|
||||
|
||||
public async onSatelliteClick(id: string = ''): Promise<void> {
|
||||
await this.$store.dispatch('nodes/selectSatellite', id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
124
web/multinode/src/app/views/PayoutsPage.vue
Normal file
124
web/multinode/src/app/views/PayoutsPage.vue
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="payouts">
|
||||
<h1 class="payouts__title">Payouts</h1>
|
||||
<div class="payouts__content-area">
|
||||
<div class="payouts__left-area">
|
||||
<div class="payouts__left-area__dropdowns">
|
||||
<satellite-selection-dropdown />
|
||||
<payout-period-calendar-button />
|
||||
</div>
|
||||
<payouts-summary-table
|
||||
class="payouts__left-area__table"
|
||||
v-if="payoutsSummary.nodeSummary"
|
||||
:node-payouts-summary="payoutsSummary.nodeSummary"
|
||||
/>
|
||||
</div>
|
||||
<div class="payouts__right-area">
|
||||
<details-area
|
||||
:total-earned="payoutsSummary.totalEarned"
|
||||
:total-held="payoutsSummary.totalHeld"
|
||||
:total-paid="payoutsSummary.totalPaid"
|
||||
/>
|
||||
<payout-history-block />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
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 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/PayoutsSummaryTable.vue';
|
||||
|
||||
import { UnauthorizedError } from '@/api';
|
||||
import { PayoutsSummary } from '@/payouts';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
PayoutPeriodCalendarButton,
|
||||
PayoutHistoryBlock,
|
||||
DetailsArea,
|
||||
PayoutsSummaryTable,
|
||||
SatelliteSelectionDropdown,
|
||||
NodesTable,
|
||||
},
|
||||
})
|
||||
export default class PayoutsPage extends Vue {
|
||||
public async mounted(): Promise<void> {
|
||||
try {
|
||||
await this.$store.dispatch('payouts/summary');
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// TODO: redirect to login screen.
|
||||
}
|
||||
|
||||
// TODO: notify error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* payoutsSummary contains payouts summary from store.
|
||||
*/
|
||||
public get payoutsSummary(): PayoutsSummary {
|
||||
return this.$store.state.payouts.summary;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.payouts {
|
||||
box-sizing: border-box;
|
||||
padding: 60px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 32px;
|
||||
color: var(--c-title);
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
&__content-area {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__left-area {
|
||||
width: 65%;
|
||||
margin-right: 32px;
|
||||
|
||||
&__dropdowns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
& > *:first-of-type {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__right-area {
|
||||
width: 35%;
|
||||
|
||||
& > *:not(:first-of-type) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,11 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* Divider to convert payout amounts to cents.
|
||||
*/
|
||||
const PRICE_DIVIDER: number = 10000;
|
||||
|
||||
/**
|
||||
* Describes node online statuses.
|
||||
*/
|
||||
@ -33,11 +38,16 @@ export class Node {
|
||||
if (now.getTime() - this.lastContact.getTime() < this.STATUS_TRESHHOLD_MILISECONDS) {
|
||||
this.status = NodeStatus.Online;
|
||||
}
|
||||
this.earned = this.convertToCents(this.earned);
|
||||
}
|
||||
|
||||
public get displayedName(): string {
|
||||
return this.name || this.id;
|
||||
}
|
||||
|
||||
private convertToCents(value: number): number {
|
||||
return value / PRICE_DIVIDER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,12 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
// TODO: move to config.
|
||||
/**
|
||||
* Divider to convert payout amounts to cents.
|
||||
*/
|
||||
const PRICE_DIVIDER: number = 10000;
|
||||
|
||||
/**
|
||||
* PayoutsSummary is a representation of summary of payout information for node.
|
||||
*/
|
||||
@ -22,7 +28,20 @@ export class PayoutsSummary {
|
||||
public totalHeld: number = 0,
|
||||
public totalPaid: number = 0,
|
||||
public nodeSummary: NodePayoutsSummary[] = [],
|
||||
) {}
|
||||
) {
|
||||
this.totalPaid = this.convertToCents(this.totalPaid);
|
||||
this.totalEarned = this.convertToCents(this.totalEarned);
|
||||
this.totalHeld = this.convertToCents(this.totalHeld);
|
||||
|
||||
this.nodeSummary.forEach((summary: NodePayoutsSummary) => {
|
||||
summary.paid = this.convertToCents(summary.paid);
|
||||
summary.held = this.convertToCents(summary.held);
|
||||
});
|
||||
}
|
||||
|
||||
private convertToCents(value: number): number {
|
||||
return value / PRICE_DIVIDER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
3
web/multinode/static/images/icons/GrayArrowLeft.svg
Normal file
3
web/multinode/static/images/icons/GrayArrowLeft.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="9" height="15" viewBox="0 0 9 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.54282 0.542969L8.95703 1.95718L3.16414 7.75008L8.95703 13.543L7.54282 14.9572L0.335711 7.75008L7.54282 0.542969Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 286 B |
@ -24,6 +24,8 @@
|
||||
--c-button-red: #ff4f4d;
|
||||
--c-button-red-hover: #de3e3d;
|
||||
--c-button-disabled: #dadde5;
|
||||
--c-button-gray: #e7e9eb;
|
||||
--c-payout-period: #444c63;
|
||||
|
||||
// borders
|
||||
--b-button-white: 1px solid #afb7c1;
|
||||
@ -32,4 +34,5 @@
|
||||
--br-button: 6px;
|
||||
--br-input: 6px;
|
||||
--br-table: 6px;
|
||||
--br-block: 6px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user