web/multinode: disk space usage and stat charts

Change-Id: Ib2b1d6f2b78d57bd43ae1152921d98a1d6c31243
This commit is contained in:
NickolaiYurchenko 2021-06-25 18:52:58 +03:00 committed by Nikolay Yurchenko
parent b900f6b4f9
commit 1230cde317
12 changed files with 591 additions and 21 deletions

View File

@ -25,6 +25,10 @@ export class BandwidthClient extends APIClient {
public async fetch(satelliteId: string | null, nodeId: string | null): Promise<BandwidthTraffic> {
let path = `${this.ROOT_PATH}`;
if (!satelliteId && !nodeId) {
path += '/';
}
if (satelliteId) {
path += `/satellites/${satelliteId}`;
}
@ -40,9 +44,10 @@ export class BandwidthClient extends APIClient {
}
const traffic = await response.json();
const daily = traffic.bandwidthDaily || [];
return new BandwidthTraffic(
traffic.bandwidthDaily.map(daily => {
daily.map(daily => {
return new BandwidthRollup(
new Egress(daily.egress.repair, daily.egress.audit, daily.egress.usage),
new Ingress(daily.ingress.repair, daily.ingress.usage),

View File

@ -33,6 +33,13 @@ export class StorageClient extends APIClient {
path += `/${nodeId}`;
}
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth() + 1;
const period = `${year}-${month > 9 ? month : `0${month}`}`;
path += `?period=${period}`;
const response = await this.http.get(path);
if (!response.ok) {

View File

@ -1,4 +1,4 @@
// Copyright (C) 2019 Storj Labs, Inc.
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>

View File

@ -1,4 +1,4 @@
// Copyright (C) 2019 Storj Labs, Inc.
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>

View File

@ -1,4 +1,4 @@
// Copyright (C) 2019 Storj Labs, Inc.
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>

View File

@ -1,4 +1,4 @@
// Copyright (C) 2019 Storj Labs, Inc.
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<script lang="ts">

View File

@ -0,0 +1,36 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<script lang="ts">
import * as VueChart from 'vue-chartjs';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { DiskStatChartData } from '@/app/types/chart';
@Component({
extends: VueChart.Doughnut,
})
export default class DoughnutChart extends Vue {
@Prop({default: () => new DiskStatChartData()})
private readonly chartData: DiskStatChartData;
@Watch('chartData')
private onDataChange(news: object, old: object): void {
(this as any).renderChart(this.chartData, {
hover: false,
tooltips: {
enabled: false,
},
});
}
public mounted(): void {
(this as any).renderChart(this.chartData, {
hover: false,
tooltips: {
enabled: false,
},
});
}
}
</script>

View File

@ -4,7 +4,7 @@
<template>
<div class="add-new-node">
<v-button :with-plus="true" label="New Node" :on-press="openModal" width="152px" />
<v-modal v-if="isAddNewNodeModalShown" @close="closeModal">
<v-modal v-if="isAddNewNodeModalShown" @onClose="closeModal">
<h2 slot="header">Add New Node</h2>
<div class="add-new-node__body" slot="body">
<headered-input

View File

@ -0,0 +1,143 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="chart">
<p class="disk-space-chart__data-dimension">{{ chartDataDimension }}*h</p>
<VChart
id="disk-space-chart"
:chart-data="chartData"
:width="chartWidth"
:height="chartHeight"
:tooltip-constructor="diskSpaceTooltip"
:key="chartKey"
/>
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import BaseChart from '@/app/components/common/BaseChart.vue';
import { ChartData, Tooltip, TooltipParams } from '@/app/types/chart';
import { Chart as ChartUtils } from '@/app/utils/chart';
import { Size } from '@/private/memory/size';
import { Stamp } from '@/storage';
/**
* stores stamp data for disc space chart's tooltip
*/
class StampTooltip {
public atRestTotal: string;
public date: string;
public constructor(stamp: Stamp) {
this.atRestTotal = Size.toBase10String(stamp.atRestTotal);
this.date = stamp.intervalStart.toUTCString().slice(0, 16);
}
}
@Component
export default class DiskSpaceChart extends BaseChart {
private get allStamps(): Stamp[] {
return ChartUtils.populateEmptyStamps(this.$store.state.storage.usage);
}
public get chartDataDimension(): string {
if (!this.$store.state.storage.usage.length) {
return 'Bytes';
}
return ChartUtils.getChartDataDimension(this.allStamps.map((elem) => {
return elem.atRestTotal;
}));
}
public get chartData(): ChartData {
let data: number[] = [0];
const daysCount = ChartUtils.daysDisplayedOnChart();
const chartBackgroundColor = '#4F97F7';
const chartBorderColor = '#1F49A3';
const chartBorderWidth = 1;
if (this.allStamps.length) {
data = ChartUtils.normalizeChartData(this.allStamps.map(elem => elem.atRestTotal));
}
return new ChartData(daysCount, chartBackgroundColor, chartBorderColor, chartBorderWidth, data);
}
public diskSpaceTooltip(tooltipModel): void {
const tooltipParams = new TooltipParams(tooltipModel, 'disk-space-chart', 'disk-space-tooltip', 'disk-space-tooltip-point', this.tooltipMarkUp(tooltipModel),
125, 89, 6, 4, `#1f49a3`);
Tooltip.custom(tooltipParams);
}
private tooltipMarkUp(tooltipModel: any): string {
if (!tooltipModel.dataPoints) {
return '';
}
const dataIndex = tooltipModel.dataPoints[0].index;
const dataPoint = new StampTooltip(this.allStamps[dataIndex]);
return `<div class='tooltip-body'>
<p class='tooltip-body__data'><b>${dataPoint.atRestTotal}*h</b></p>
<p class='tooltip-body__footer'>${dataPoint.date}</p>
</div>`;
}
}
</script>
<style lang="scss">
p {
margin: 0;
}
.disk-space-chart {
z-index: 102;
&__data-dimension {
font-size: 13px;
color: var(--c-title);
margin: 0 0 5px 31px !important;
font-family: 'font_medium', sans-serif;
}
}
#disk-space-tooltip {
background: white;
border: 1px solid var(--c-gray--light);
width: 180px;
height: 90px;
font-size: 12px;
border-radius: 14px;
color: var(--c-title);
pointer-events: none;
z-index: 9999;
font-family: 'font_bold', sans-serif;
}
.tooltip-body {
&__data {
display: flex;
align-items: center;
justify-content: center;
padding: 11px 44px;
font-size: 14px;
}
&__footer {
font-size: 12px;
width: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
}
}
</style>

View File

@ -0,0 +1,275 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="disk-stat-area">
<p class="disk-stat-area__title">Total Disk Space</p>
<p class="disk-stat-area__amount">{{ diskSpace.allocated | bytesToBase10String }}</p>
<doughnut-chart class="disk-stat-area__chart" :chart-data="chartData" />
<div class="disk-stat-area__info-area">
<div class="disk-stat-area__info-area__item">
<div class="disk-stat-area__info-area__item__labels-area">
<div class="disk-stat-area__info-area__item__labels-area__circle used"></div>
<p class="disk-stat-area__info-area__item__labels-area__label">Used</p>
</div>
<p class="disk-stat-area__info-area__item__labels-area__amount">{{ diskSpace.usedPieces | bytesToBase10String }}</p>
</div>
<div class="disk-stat-area__info-area__item">
<div class="disk-stat-area__info-area__item__labels-area">
<div class="disk-stat-area__info-area__item__labels-area__circle free"></div>
<p class="disk-stat-area__info-area__item__labels-area__label">Free</p>
</div>
<p class="disk-stat-area__info-area__item__labels-area__amount">{{ diskSpace.free | bytesToBase10String }}</p>
</div>
<div class="disk-stat-area__info-area__item">
<div class="disk-stat-area__info-area__item__labels-area">
<div class="disk-stat-area__info-area__item__labels-area__circle trash"></div>
<p class="disk-stat-area__info-area__item__labels-area__label">Trash</p>
</div>
<p class="disk-stat-area__info-area__item__labels-area__amount">{{ diskSpace.usedTrash | bytesToBase10String }}</p>
</div>
<div class="disk-stat-area__info-area__item">
<div class="disk-stat-area__info-area__item__labels-area">
<div class="disk-stat-area__info-area__item__labels-area__circle overused"></div>
<p class="disk-stat-area__info-area__item__labels-area__label">Overused</p>
</div>
<p class="disk-stat-area__info-area__item__labels-area__amount">{{ diskSpace.overused | bytesToBase10String }}</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import DoughnutChart from '@/app/components/common/DoughnutChart.vue';
import { DiskStatChartData, DiskStatDataSet } from '@/app/types/chart';
import { DiskSpace } from '@/storage';
@Component({
components: { DoughnutChart },
})
export default class DiskStatChart extends Vue {
/**
* Holds datasets for chart.
*/
public get chartData(): DiskStatChartData {
return new DiskStatChartData([
new DiskStatDataSet(
'',
['#D6D6D6', '#0059D0', '#8FA7C6', '#EB5757'],
[
this.diskSpace.free,
this.diskSpace.usedPieces,
this.diskSpace.usedTrash,
this.diskSpace.overused,
],
),
]);
}
/**
* Returns disk space information from store.
*/
public get diskSpace(): DiskSpace {
return this.$store.state.storage.diskSpace;
}
}
</script>
<style lang="scss">
.disk-stat-area {
width: 400px;
height: 336px;
background-color: white;
border: 1px solid var(--c-gray--light);
border-radius: 11px;
padding: 32px 20px;
position: relative;
&__title {
font-family: 'font_regular', sans-serif;
font-size: 14px;
color: var(--c-gray);
user-select: none;
}
&__amount {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 57px;
color: var(--c-title);
margin-top: 5px;
}
&__chart {
position: absolute;
width: calc(58% - 25px);
height: 220px;
top: 135px;
}
&__info-area {
position: absolute;
right: 30px;
top: 60%;
transform: translateY(-50%);
width: calc(40% - 35px);
display: flex;
flex-direction: column;
box-sizing: border-box;
&__item {
display: flex;
justify-content: space-between;
flex-direction: column;
margin-top: 19px;
&:first-of-type {
margin-top: 0;
}
&__labels-area {
display: flex;
align-items: center;
&__circle {
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 8px;
}
&__label {
font-family: 'font_regular', sans-serif;
font-size: 14px;
color: #586474;
}
&__amount {
font-family: 'font_bold', sans-serif;
font-weight: bold;
font-size: 14px;
color: var(--c-title);
margin-left: 22px;
margin-top: 6px;
}
}
}
}
}
.used {
background: #0059d0;
}
.free {
background: #d6d6d6;
}
.trash {
background: #8fa7c6;
}
.overused {
background: #eb5757;
}
@media screen and (max-width: 1000px) {
.disk-stat-area {
width: calc(100% - 60px);
&__chart {
width: 250px;
height: 250px;
margin-left: 100px;
top: 100px;
}
&__info-area {
right: 120px;
width: 185px;
&__item {
flex-direction: row;
&__labels-area__amount {
margin: 0;
}
}
}
}
}
@media screen and (max-width: 780px) {
.disk-stat-area {
&__chart {
margin-left: 50px;
}
&__info-area {
right: 90px;
width: 140px;
&__item {
flex-direction: row;
&__labels-area__amount {
margin: 0;
}
}
}
}
}
@media screen and (max-width: 640px) {
.disk-stat-area {
&__chart {
top: 125px;
width: 200px;
height: 200px;
margin-left: 50px;
}
&__info-area {
top: 55%;
right: 90px;
width: 140px;
}
}
}
@media screen and (max-width: 550px) {
.disk-stat-area {
height: 414px;
width: calc(100% - 36px);
padding: 24px 18px;
&__chart {
top: 100px;
width: 200px;
height: 200px;
left: 50%;
transform: translateX(-50%);
margin: 0;
}
&__info-area {
top: 70%;
right: 50%;
transform: translateX(50%);
bottom: 10px;
height: 100px;
width: 200px;
}
}
}
</style>

View File

@ -8,7 +8,7 @@
<node-selection-dropdown />
<satellite-selection-dropdown />
</div>
<div class="chart-container">
<div class="chart-container bandwidth-chart">
<div class="chart-container__title-area">
<p class="chart-container__title-area__title">Bandwidth Used This Month</p>
<div class="chart-container__title-area__buttons-area">
@ -42,11 +42,25 @@
<p class="chart-container__amount" v-else-if="isIngressChartShown"><b>{{ bandwidth.ingressSummary | bytesToBase10String }}</b></p>
<p class="chart-container__amount" v-else><b>{{ bandwidth.bandwidthSummary | bytesToBase10String }}</b></p>
<div class="chart-container__chart" ref="chart" onresize="recalculateChartDimensions()" >
<EgressChart v-if="isEgressChartShown" :height="chartHeight" :width="chartWidth"/>
<IngressChart v-else-if="isIngressChartShown" :height="chartHeight" :width="chartWidth"/>
<BandwidthChart v-else :height="chartHeight" :width="chartWidth"/>
<egress-chart v-if="isEgressChartShown" :height="chartHeight" :width="chartWidth"/>
<ingress-chart v-else-if="isIngressChartShown" :height="chartHeight" :width="chartWidth"/>
<bandwidth-chart v-else :height="chartHeight" :width="chartWidth"/>
</div>
</div>
<section class="bandwidth__chart-area">
<section class="chart-container">
<div class="chart-container__title-area disk-space-title">
<p class="chart-container__title-area__title">Disk Space Used This Month</p>
</div>
<p class="chart-container__amount disk-space-amount"><b>{{ 0 | bytesToBase10String }}*h</b></p>
<div class="chart-container__chart" ref="diskSpaceChart" onresize="recalculateChartDimensions()" >
<disk-space-chart :height="diskSpaceChartHeight" :width="diskSpaceChartWidth"/>
</div>
</section>
<section class="disk-stat-chart">
<disk-stat-chart />
</section>
</section>
</div>
</template>
@ -58,12 +72,16 @@ import EgressChart from '@/app/components/bandwidth/EgressChart.vue';
import IngressChart from '@/app/components/bandwidth/IngressChart.vue';
import NodeSelectionDropdown from '@/app/components/common/NodeSelectionDropdown.vue';
import SatelliteSelectionDropdown from '@/app/components/common/SatelliteSelectionDropdown.vue';
import DiskSpaceChart from '@/app/components/storage/DiskSpaceChart.vue';
import DiskStatChart from '@/app/components/storage/DiskStatChart.vue';
import { UnauthorizedError } from '@/api';
import { BandwidthTraffic } from '@/bandwidth';
@Component({
components: {
DiskStatChart,
DiskSpaceChart,
EgressChart,
IngressChart,
BandwidthChart,
@ -74,6 +92,8 @@ import { BandwidthTraffic } from '@/bandwidth';
export default class BandwidthPage extends Vue {
public chartWidth: number = 0;
public chartHeight: number = 0;
public diskSpaceChartWidth: number = 0;
public diskSpaceChartHeight: number = 0;
public isEgressChartShown: boolean = false;
public isIngressChartShown: boolean = false;
public $refs: {
@ -90,6 +110,8 @@ export default class BandwidthPage extends Vue {
public recalculateChartDimensions(): void {
this.chartWidth = this.$refs['chart'].clientWidth;
this.chartHeight = this.$refs['chart'].clientHeight;
this.diskSpaceChartWidth = this.$refs['diskSpaceChart'].clientWidth;
this.diskSpaceChartHeight = this.$refs['diskSpaceChart'].clientHeight;
}
/**
@ -109,14 +131,14 @@ export default class BandwidthPage extends Vue {
// TODO: notify error
}
await this.fetchBandwidth();
await this.fetchTraffic();
// Subscribes on period or satellite change
this.$store.subscribe(async (mutation) => {
const watchedMutations = [ 'nodes/setSelectedNode', 'nodes/setSelectedSatellite' ];
if (watchedMutations.includes(mutation.type)) {
await this.fetchBandwidth();
await this.fetchTraffic();
}
});
@ -156,9 +178,9 @@ export default class BandwidthPage extends Vue {
}
/**
* Fetches bandwidth information.
* Fetches bandwidth and disk space information.
*/
private async fetchBandwidth(): Promise<void> {
private async fetchTraffic(): Promise<void> {
try {
await this.$store.dispatch('bandwidth/fetch');
} catch (error) {
@ -168,6 +190,26 @@ export default class BandwidthPage extends Vue {
// TODO: notify error
}
try {
await this.$store.dispatch('storage/usage');
} catch (error) {
if (error instanceof UnauthorizedError) {
// TODO: redirect to login screen.
}
// TODO: notify error
}
try {
await this.$store.dispatch('storage/diskSpace');
} catch (error) {
if (error instanceof UnauthorizedError) {
// TODO: redirect to login screen.
}
// TODO: notify error
}
}
}
</script>
@ -203,7 +245,7 @@ export default class BandwidthPage extends Vue {
& .chart-container {
box-sizing: border-box;
width: 100%;
width: 65%;
height: 401px;
background-color: white;
border: 1px solid var(--c-gray--light);
@ -263,5 +305,25 @@ export default class BandwidthPage extends Vue {
height: 240px;
}
}
&__chart-area {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
}
.disk-space-amount {
margin-top: 5px;
}
.bandwidth-chart {
width: 100% !important;
}
.disk-stat-chart {
margin: 20px 0 13px 0;
width: auto;
}
</style>

View File

@ -7,7 +7,7 @@ exports[`BandwidthPage renders correctly 1`] = `
<node-selection-dropdown-stub></node-selection-dropdown-stub>
<satellite-selection-dropdown-stub></satellite-selection-dropdown-stub>
</div>
<div class="chart-container">
<div class="chart-container bandwidth-chart">
<div class="chart-container__title-area">
<p class="chart-container__title-area__title">Bandwidth Used This Month</p>
<div class="chart-container__title-area__buttons-area"><button name="Show Bandwidth Chart" class="chart-container__title-area__chart-choice-item active">
@ -20,9 +20,23 @@ exports[`BandwidthPage renders correctly 1`] = `
</div>
<p class="chart-container__amount"><b>0.7GB</b></p>
<div onresize="recalculateChartDimensions()" class="chart-container__chart">
<bandwidthchart-stub width="0" height="0"></bandwidthchart-stub>
<bandwidth-chart-stub width="0" height="0"></bandwidth-chart-stub>
</div>
</div>
<section class="bandwidth__chart-area">
<section class="chart-container">
<div class="chart-container__title-area disk-space-title">
<p class="chart-container__title-area__title">Disk Space Used This Month</p>
</div>
<p class="chart-container__amount disk-space-amount"><b>0B*h</b></p>
<div onresize="recalculateChartDimensions()" class="chart-container__chart">
<disk-space-chart-stub width="0" height="0"></disk-space-chart-stub>
</div>
</section>
<section class="disk-stat-chart">
<disk-stat-chart-stub></disk-stat-chart-stub>
</section>
</section>
</div>
`;
@ -33,7 +47,7 @@ exports[`BandwidthPage renders correctly with egress chart 1`] = `
<node-selection-dropdown-stub></node-selection-dropdown-stub>
<satellite-selection-dropdown-stub></satellite-selection-dropdown-stub>
</div>
<div class="chart-container">
<div class="chart-container bandwidth-chart">
<div class="chart-container__title-area">
<p class="chart-container__title-area__title">Bandwidth Used This Month</p>
<div class="chart-container__title-area__buttons-area"><button name="Show Bandwidth Chart" class="chart-container__title-area__chart-choice-item">
@ -46,9 +60,23 @@ exports[`BandwidthPage renders correctly with egress chart 1`] = `
</div>
<p class="chart-container__amount"><b>577.7GB</b></p>
<div onresize="recalculateChartDimensions()" class="chart-container__chart">
<egresschart-stub width="0" height="0"></egresschart-stub>
<egress-chart-stub width="0" height="0"></egress-chart-stub>
</div>
</div>
<section class="bandwidth__chart-area">
<section class="chart-container">
<div class="chart-container__title-area disk-space-title">
<p class="chart-container__title-area__title">Disk Space Used This Month</p>
</div>
<p class="chart-container__amount disk-space-amount"><b>0B*h</b></p>
<div onresize="recalculateChartDimensions()" class="chart-container__chart">
<disk-space-chart-stub width="0" height="0"></disk-space-chart-stub>
</div>
</section>
<section class="disk-stat-chart">
<disk-stat-chart-stub></disk-stat-chart-stub>
</section>
</section>
</div>
`;
@ -59,7 +87,7 @@ exports[`BandwidthPage renders correctly with ingress chart 1`] = `
<node-selection-dropdown-stub></node-selection-dropdown-stub>
<satellite-selection-dropdown-stub></satellite-selection-dropdown-stub>
</div>
<div class="chart-container">
<div class="chart-container bandwidth-chart">
<div class="chart-container__title-area">
<p class="chart-container__title-area__title">Bandwidth Used This Month</p>
<div class="chart-container__title-area__buttons-area"><button name="Show Bandwidth Chart" class="chart-container__title-area__chart-choice-item">
@ -72,8 +100,22 @@ exports[`BandwidthPage renders correctly with ingress chart 1`] = `
</div>
<p class="chart-container__amount"><b>5MB</b></p>
<div onresize="recalculateChartDimensions()" class="chart-container__chart">
<ingresschart-stub width="0" height="0"></ingresschart-stub>
<ingress-chart-stub width="0" height="0"></ingress-chart-stub>
</div>
</div>
<section class="bandwidth__chart-area">
<section class="chart-container">
<div class="chart-container__title-area disk-space-title">
<p class="chart-container__title-area__title">Disk Space Used This Month</p>
</div>
<p class="chart-container__amount disk-space-amount"><b>0B*h</b></p>
<div onresize="recalculateChartDimensions()" class="chart-container__chart">
<disk-space-chart-stub width="0" height="0"></disk-space-chart-stub>
</div>
</section>
<section class="disk-stat-chart">
<disk-stat-chart-stub></disk-stat-chart-stub>
</section>
</section>
</div>
`;