web/multinode: bandwidth charts added

bandwidth/egress/ingress chart and api/vuex connection

Change-Id: I16ba2bb82854a1d198384b3b8e6ffc4e58d8bb91
This commit is contained in:
NickolaiYurchenko 2021-06-10 15:51:10 +03:00 committed by Nikolay Yurchenko
parent cc5de4288b
commit cece8e4110
13 changed files with 1377 additions and 3 deletions

View File

@ -4234,6 +4234,32 @@
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
"dev": true "dev": true
}, },
"chart.js": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz",
"integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==",
"requires": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"chartjs-color": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
"requires": {
"chartjs-color-string": "^0.6.0",
"color-convert": "^1.9.3"
}
},
"chartjs-color-string": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
"requires": {
"color-name": "^1.0.0"
}
},
"check-types": { "check-types": {
"version": "8.0.3", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
@ -10471,6 +10497,11 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
}, },
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -16003,6 +16034,11 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
}, },
"vue-chartjs": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.0.tgz",
"integrity": "sha512-yWNhG3B6g6lvYqNInP0WaDWNZG/SNb6XnltkjR0wYC5pmLm6jvdiotj8er7Mui8qkJGfLZe6ULjrZdHWjegAUg=="
},
"vue-class-component": { "vue-class-component": {
"version": "7.2.6", "version": "7.2.6",
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz", "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz",

View File

@ -9,7 +9,9 @@
"test": "vue-cli-service test:unit" "test": "vue-cli-service test:unit"
}, },
"dependencies": { "dependencies": {
"chart.js": "2.9.3",
"vue": "2.6.11", "vue": "2.6.11",
"vue-chartjs": "3.5.0",
"vue-class-component": "7.2.6", "vue-class-component": "7.2.6",
"vue-clipboard2": "0.3.1", "vue-clipboard2": "0.3.1",
"vue-jest": "3.0.5", "vue-jest": "3.0.5",

View File

@ -0,0 +1,207 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="chart">
<p class="bandwidth-chart__data-dimension">{{ chartDataDimension }}</p>
<VChart
id="bandwidth-chart"
:chart-data="chartData"
:width="chartWidth"
:height="chartHeight"
:tooltip-constructor="bandwidthTooltip"
: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 { BandwidthRollup } from '@/bandwidth';
import { Size } from '@/private/memory/size';
/**
* stores bandwidth data for bandwidth chart's tooltip
*/
class BandwidthTooltip {
public normalEgress: string;
public normalIngress: string;
public repairIngress: string;
public repairEgress: string;
public auditEgress: string;
public date: string;
public constructor(bandwidth: BandwidthRollup) {
this.normalEgress = Size.toBase10String(bandwidth.egress.usage);
this.normalIngress = Size.toBase10String(bandwidth.ingress.usage);
this.repairIngress = Size.toBase10String(bandwidth.ingress.repair);
this.repairEgress = Size.toBase10String(bandwidth.egress.repair);
this.auditEgress = Size.toBase10String(bandwidth.egress.audit);
this.date = bandwidth.intervalStart.toUTCString().slice(0, 16);
}
}
@Component
export default class BandwidthChart extends BaseChart {
private get allBandwidth(): BandwidthRollup[] {
return ChartUtils.populateEmptyBandwidth(this.$store.state.bandwidth.traffic.bandwidthDaily);
}
public get chartDataDimension(): string {
if (!this.$store.state.bandwidth.traffic.bandwidthDaily.length) {
return 'Bytes';
}
return ChartUtils.getChartDataDimension(this.allBandwidth.map((elem) => {
return elem.egress.usage + elem.egress.repair + elem.egress.audit
+ elem.ingress.repair + elem.ingress.usage;
}));
}
public get chartData(): ChartData {
let data: number[] = [0];
const daysCount = ChartUtils.daysDisplayedOnChart();
const chartBackgroundColor = '#F2F6FC';
const chartBorderColor = '#1F49A3';
const chartBorderWidth = 1;
if (this.allBandwidth.length) {
data = ChartUtils.normalizeChartData(this.allBandwidth.map(elem => {
return elem.egress.usage + elem.egress.repair + elem.egress.audit
+ elem.ingress.repair + elem.ingress.usage;
}));
}
return new ChartData(daysCount, chartBackgroundColor, chartBorderColor, chartBorderWidth, data);
}
public bandwidthTooltip(tooltipModel: any): void {
const tooltipParams = new TooltipParams(tooltipModel, 'bandwidth-chart', 'bandwidth-tooltip',
'bandwidth-tooltip-point', this.tooltipMarkUp(tooltipModel),
285, 125, 6, 4, `#1f49a3`);
Tooltip.custom(tooltipParams);
}
private tooltipMarkUp(tooltipModel: any): string {
if (!tooltipModel.dataPoints) {
return '';
}
const dataIndex = tooltipModel.dataPoints[0].index;
const dataPoint = new BandwidthTooltip(this.allBandwidth[dataIndex]);
return `<div class='tooltip-header'>
<p>EGRESS</p>
<p class='tooltip-header__ingress'>INGRESS</p>
</div>
<div class='tooltip-body'>
<div class='tooltip-body__info'>
<p>USAGE</p>
<p class='tooltip-body__info__egress-value'><b class="tooltip-bold-text">${dataPoint.normalEgress}</b></p>
<p class='tooltip-body__info__ingress-value'><b class="tooltip-bold-text">${dataPoint.normalIngress}</b></p>
</div>
<div class='tooltip-body__info'>
<p>REPAIR</p>
<p class='tooltip-body__info__egress-value'><b class="tooltip-bold-text">${dataPoint.repairEgress}</b></p>
<p class='tooltip-body__info__ingress-value'><b class="tooltip-bold-text">${dataPoint.repairIngress}</b></p>
</div>
<div class='tooltip-body__info'>
<p>AUDIT</p>
<p class='tooltip-body__info__egress-value'><b class="tooltip-bold-text">${dataPoint.auditEgress}</b></p>
</div>
</div>
<div class='tooltip-footer'>
<p>${dataPoint.date}</p>
</div>`;
}
}
</script>
<style lang="scss">
p {
margin: 0;
}
.bandwidth-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;
}
}
#bandwidth-tooltip {
background: white;
border: 1px solid var(--c-gray--light);
min-width: 250px;
min-height: 230px;
font-size: 12px;
border-radius: 14px;
font-family: 'font_bold', sans-serif;
color: var(--c-title);
pointer-events: none;
z-index: 9999;
}
#bandwidth-tooltip-point {
z-index: 9999;
}
.tooltip-header {
display: flex;
padding: 10px 0 0 92px;
line-height: 40px;
&__ingress {
margin-left: 29px;
}
}
.tooltip-body {
margin: 8px;
&__info {
display: flex;
border-radius: 12px;
padding: 14px 17px 14px 14px;
align-items: center;
margin-bottom: 14px;
position: relative;
font-family: 'font_bold', sans-serif;
.tooltip-bold-text {
color: #1f49a3;
font-size: 14px;
}
&__egress-value {
position: absolute;
left: 83px;
}
&__ingress-value {
position: absolute;
left: 158px;
}
}
}
.tooltip-footer {
font-size: 12px;
width: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0 16px 0;
color: var(--c-title);
}
</style>

View File

@ -0,0 +1,168 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="chart">
<p class="egress-chart__data-dimension">{{ chartDataDimension }}</p>
<VChart
id="egress-chart"
:chart-data="chartData"
:width="chartWidth"
:height="chartHeight"
:tooltip-constructor="egressTooltip"
: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 { BandwidthRollup } from '@/bandwidth';
import { Size } from '@/private/memory/size';
/**
* stores egress data for egress bandwidth chart's tooltip
*/
class EgressTooltip {
public normalEgress: string;
public repairEgress: string;
public auditEgress: string;
public date: string;
public constructor(bandwidth: BandwidthRollup) {
this.normalEgress = Size.toBase10String(bandwidth.egress.usage);
this.repairEgress = Size.toBase10String(bandwidth.egress.repair);
this.auditEgress = Size.toBase10String(bandwidth.egress.audit);
this.date = bandwidth.intervalStart.toUTCString().slice(0, 16);
}
}
@Component
export default class EgressChart extends BaseChart {
private get allBandwidth(): BandwidthRollup[] {
return ChartUtils.populateEmptyBandwidth(this.$store.state.bandwidth.traffic.bandwidthDaily);
}
public get chartDataDimension(): string {
if (!this.$store.state.bandwidth.traffic.bandwidthDaily.length) {
return 'Bytes';
}
return ChartUtils.getChartDataDimension(this.allBandwidth.map((elem) => {
return elem.egress.audit + elem.egress.repair + elem.egress.usage;
}));
}
public get chartData(): ChartData {
let data: number[] = [0];
const daysCount = ChartUtils.daysDisplayedOnChart();
const chartBackgroundColor = '#edf9f4';
const chartBorderColor = '#48a77f';
const chartBorderWidth = 1;
if (this.allBandwidth.length) {
data = ChartUtils.normalizeChartData(this.allBandwidth.map(elem => elem.egress.audit + elem.egress.repair + elem.egress.usage));
}
return new ChartData(daysCount, chartBackgroundColor, chartBorderColor, chartBorderWidth, data);
}
public egressTooltip(tooltipModel): void {
const tooltipParams = new TooltipParams(tooltipModel, 'egress-chart', 'egress-tooltip',
'egress-tooltip-point', this.tooltipMarkUp(tooltipModel),
235, 94, 6, 4, `#48a77f`);
Tooltip.custom(tooltipParams);
}
private tooltipMarkUp(tooltipModel: any): string {
if (!tooltipModel.dataPoints) {
return '';
}
const dataIndex = tooltipModel.dataPoints[0].index;
const dataPoint = new EgressTooltip(this.allBandwidth[dataIndex]);
return `<div class='egress-tooltip-body'>
<div class='egress-tooltip-body__info'>
<p>USAGE</p>
<b class="egress-tooltip-bold-text">${dataPoint.normalEgress}</b>
</div>
<div class='egress-tooltip-body__info'>
<p>REPAIR</p>
<b class="egress-tooltip-bold-text">${dataPoint.repairEgress}</b>
</div>
<div class='egress-tooltip-body__info'>
<p>AUDIT</p>
<b class="egress-tooltip-bold-text">${dataPoint.auditEgress}</b>
</div>
</div>
<div class='egress-tooltip-footer'>
<p>${dataPoint.date}</p>
</div>`;
}
}
</script>
<style lang="scss">
.egress-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;
}
}
#egress-tooltip {
background: white;
border: 1px solid var(--c-gray--light);
min-width: 190px;
min-height: 170px;
font-size: 12px;
border-radius: 14px;
font-family: 'font_bold', sans-serif;
color: var(--c-title);
pointer-events: none;
z-index: 9999;
}
.egress-tooltip-body {
margin: 8px;
&__info {
display: flex;
border-radius: 12px;
padding: 14px;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
position: relative;
font-family: 'font_bold', sans-serif;
}
}
.egress-tooltip-bold-text {
color: #2e5f46;
font-size: 14px;
}
.egress-tooltip-footer {
position: relative;
font-size: 12px;
width: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0 16px 0;
color: var(--c-title);
font-family: 'font_bold', sans-serif;
}
</style>

View File

@ -0,0 +1,166 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="chart">
<p class="ingress-chart__data-dimension">{{ chartDataDimension }}</p>
<VChart
id="ingress-chart"
:chart-data="chartData"
:width="chartWidth"
:height="chartHeight"
:tooltip-constructor="ingressTooltip"
: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 { BandwidthRollup } from '@/bandwidth';
import { Size } from '@/private/memory/size';
/**
* stores ingress data for ingress bandwidth chart's tooltip
*/
class IngressTooltip {
public normalIngress: string;
public repairIngress: string;
public date: string;
public constructor(bandwidth: BandwidthRollup) {
this.normalIngress = Size.toBase10String(bandwidth.ingress.usage);
this.repairIngress = Size.toBase10String(bandwidth.ingress.repair);
this.date = bandwidth.intervalStart.toUTCString().slice(0, 16);
}
}
@Component
export default class IngressChart extends BaseChart {
private get allBandwidth(): BandwidthRollup[] {
return ChartUtils.populateEmptyBandwidth(this.$store.state.bandwidth.traffic.bandwidthDaily);
}
public get chartDataDimension(): string {
if (!this.$store.state.bandwidth.traffic.bandwidthDaily.length) {
return 'Bytes';
}
return ChartUtils.getChartDataDimension(this.allBandwidth.map((elem) => {
return elem.ingress.repair + elem.ingress.usage;
}));
}
public get chartData(): ChartData {
let data: number[] = [0];
const daysCount = ChartUtils.daysDisplayedOnChart();
const chartBackgroundColor = '#fff4df';
const chartBorderColor = '#e1a128';
const chartBorderWidth = 1;
if (this.allBandwidth.length) {
data = ChartUtils.normalizeChartData(this.allBandwidth.map(elem => elem.ingress.repair + elem.ingress.usage));
}
return new ChartData(daysCount, chartBackgroundColor, chartBorderColor, chartBorderWidth, data);
}
public ingressTooltip(tooltipModel): void {
const tooltipParams = new TooltipParams(tooltipModel, 'ingress-chart', 'ingress-tooltip',
'ingress-tooltip-point', this.tooltipMarkUp(tooltipModel),
185, 94, 6, 4, `#e1a128`);
Tooltip.custom(tooltipParams);
}
private tooltipMarkUp(tooltipModel: any): string {
if (!tooltipModel.dataPoints) {
return '';
}
const dataIndex = tooltipModel.dataPoints[0].index;
const dataPoint = new IngressTooltip(this.allBandwidth[dataIndex]);
return `<div class='ingress-tooltip-body'>
<div class='ingress-tooltip-body__info'>
<p>USAGE</p>
<b class="ingress-tooltip-bold-text">${dataPoint.normalIngress}</b>
</div>
<div class='ingress-tooltip-body__info'>
<p>REPAIR</p>
<b class="ingress-tooltip-bold-text">${dataPoint.repairIngress}</b>
</div>
</div>
<div class='ingress-tooltip-footer'>
<p>${dataPoint.date}</p>
</div>`;
}
}
</script>
<style lang="scss">
.ingress-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;
}
}
#ingress-tooltip {
background: white;
border: 1px solid var(--c-gray--light);
min-width: 190px;
min-height: 170px;
font-size: 12px;
border-radius: 14px;
font-family: 'font_bold', sans-serif;
color: var(--c-title);
pointer-events: none;
z-index: 9999;
}
#ingress-tooltip-point {
z-index: 9999;
}
.ingress-tooltip-body {
margin: 8px;
&__info {
display: flex;
border-radius: 12px;
padding: 14px;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
position: relative;
font-family: 'font_bold', sans-serif;
}
}
.ingress-tooltip-bold-text {
color: #6e4f15;
font-size: 14px;
}
.ingress-tooltip-footer {
position: relative;
font-size: 12px;
width: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0 16px 0;
color: var(--c-title);
font-family: 'font_bold', sans-serif;
}
</style>

View File

@ -0,0 +1,63 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import VChart from '@/app/components/common/VChart.vue';
@Component ({
components: {
VChart,
},
})
export default class BaseChart extends Vue {
@Prop({default: 0})
public width: number;
@Prop({default: 0})
public height: number;
@Prop({default: false})
public isDarkMode: boolean;
public chartWidth: number = this.width;
public chartHeight: number = this.height;
/**
* Used for chart re rendering.
*/
public chartKey: number = 0;
public $refs: {
chartContainer: HTMLElement;
};
@Watch('width')
@Watch('isDarkMode')
public rebuildChart(): void {
this.chartHeight = this.height;
this.chartWidth = this.width;
this.chartKey += 1;
}
}
</script>
<style scoped lang="scss">
.chart {
width: 100%;
height: 100%;
&__data-dimension {
font-size: 13px;
color: #586c86;
margin: 0 0 5px 31px !important;
font-family: 'font_medium', sans-serif;
}
}
@media screen and (max-width: 400px) {
.chart-container {
width: calc(100% - 36px);
padding: 24px 18px;
}
}
</style>

View File

@ -0,0 +1,148 @@
// 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 { ChartData } from '@/app/types/chart';
class DayShowingConditions {
public readonly day: string;
public readonly daysArray: string[];
public constructor(day: string, daysArray: string[]) {
this.day = day;
this.daysArray = daysArray;
}
public countMiddleDateValue(): number {
return this.daysArray.length / 2;
}
public isDayFirstOrLast(): boolean {
return this.day === this.daysArray[0] || this.day === this.daysArray[this.daysArray.length - 1];
}
public isDayAfterEighthDayOfTheMonth(): boolean {
return this.daysArray.length > 8 && this.daysArray.length <= 31;
}
}
@Component({
extends: VueChart.Line,
})
export default class VChart extends Vue {
@Prop({default: '$'})
private readonly currency: string;
@Prop({default: () => { console.error('Tooltip constructor is undefined'); } })
private tooltipConstructor: (tooltipModel) => void;
@Prop({default: {}})
private readonly chartData: ChartData;
@Watch('chartData')
private onDataChange(news: object, old: object) {
/**
* renderChart method is inherited from BaseChart which is extended by VChart.Line
*/
(this as any).renderChart(this.chartData, this.chartOptions);
}
public mounted(): void {
/**
* renderChart method is inherited from BaseChart which is extended by VChart.Line
*/
(this as any).renderChart(this.chartData, this.chartOptions);
}
public get chartOptions(): object {
const filterCallback = this.filterDaysDisplayed;
return {
responsive: false,
maintainAspectRatios: false,
legend: {
display: false,
},
elements: {
point: {
radius: 0,
hoverRadius: 0,
hitRadius: 500,
},
},
scales: {
yAxes: [{
display: true,
ticks: {
beginAtZero: true,
color: '#586C86',
},
gridLines: {
borderDash: [2, 5],
drawBorder: false,
},
}],
xAxes: [{
display: true,
ticks: {
fontFamily: 'font_regular',
autoSkip: false,
maxRotation: 0,
minRotation: 0,
callback: filterCallback,
},
gridLines: {
display: false,
},
}],
},
layout: {
padding: {
left: 25,
},
},
tooltips: {
enabled: false,
custom: ((tooltipModel) => {
this.tooltipConstructor(tooltipModel);
}),
labels: {
enabled: true,
},
},
};
}
private filterDaysDisplayed(day: string, dayIndex: string, labelArray: string[]): string | undefined {
const eighthDayOfTheMonth = 8;
const isBeforeEighthDayOfTheMonth = labelArray.length <= eighthDayOfTheMonth;
const dayShowingConditions = new DayShowingConditions(day, labelArray);
if (isBeforeEighthDayOfTheMonth || this.areDaysShownOnEvenDaysAmount(dayShowingConditions)
|| this.areDaysShownOnNotEvenDaysAmount(dayShowingConditions)) {
return day;
}
}
private areDaysShownOnEvenDaysAmount(dayShowingConditions: DayShowingConditions): boolean {
const isDaysAmountEven = dayShowingConditions.daysArray.length % 2 === 0;
const isDateValueInMiddleInEvenAmount = dayShowingConditions.day ===
dayShowingConditions.daysArray[dayShowingConditions.countMiddleDateValue() - 1];
return dayShowingConditions.isDayFirstOrLast() || (isDaysAmountEven
&& dayShowingConditions.isDayAfterEighthDayOfTheMonth() && isDateValueInMiddleInEvenAmount);
}
private areDaysShownOnNotEvenDaysAmount(dayShowingConditions: DayShowingConditions): boolean {
const isDaysAmountNotEven = dayShowingConditions.daysArray.length % 2 !== 0;
const isDateValueInMiddleInNotEvenAmount = dayShowingConditions.day
=== dayShowingConditions.daysArray[Math.floor(dayShowingConditions.countMiddleDateValue())];
return dayShowingConditions.isDayFirstOrLast() || (isDaysAmountNotEven
&& dayShowingConditions.isDayAfterEighthDayOfTheMonth() && isDateValueInMiddleInNotEvenAmount);
}
}
</script>

View File

@ -4,12 +4,15 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex, { ModuleTree, Store, StoreOptions } from 'vuex'; import Vuex, { ModuleTree, Store, StoreOptions } from 'vuex';
import { BandwidthClient } from '@/api/bandwidth';
import { NodesClient } from '@/api/nodes'; import { NodesClient } from '@/api/nodes';
import { Operators as OperatorsClient } from '@/api/operators'; import { Operators as OperatorsClient } from '@/api/operators';
import { PayoutsClient } from '@/api/payouts'; import { PayoutsClient } from '@/api/payouts';
import { BandwidthModule, BandwidthState } from '@/app/store/bandwidth';
import { NodesModule, NodesState } from '@/app/store/nodes'; import { NodesModule, NodesState } from '@/app/store/nodes';
import { OperatorsModule, OperatorsState } from '@/app/store/operators'; import { OperatorsModule, OperatorsState } from '@/app/store/operators';
import { PayoutsModule, PayoutsState } from '@/app/store/payouts'; import { PayoutsModule, PayoutsState } from '@/app/store/payouts';
import { Bandwidth } from '@/bandwidth/service';
import { Nodes } from '@/nodes/service'; import { Nodes } from '@/nodes/service';
import { Operators } from '@/operators'; import { Operators } from '@/operators';
import { Payouts } from '@/payouts/service'; import { Payouts } from '@/payouts/service';
@ -23,6 +26,7 @@ export class RootState {
nodes: NodesState; nodes: NodesState;
payouts: PayoutsState; payouts: PayoutsState;
operators: OperatorsState; operators: OperatorsState;
bandwidth: BandwidthState;
} }
/** /**
@ -33,16 +37,23 @@ export class MultinodeStoreOptions implements StoreOptions<RootState> {
public readonly state: RootState; public readonly state: RootState;
public readonly modules: ModuleTree<RootState>; public readonly modules: ModuleTree<RootState>;
public constructor(nodes: NodesModule, payouts: PayoutsModule, operators: OperatorsModule) { public constructor(
nodes: NodesModule,
payouts: PayoutsModule,
operators: OperatorsModule,
bandwidth: BandwidthModule,
) {
this.strict = true; this.strict = true;
this.state = { this.state = {
nodes: nodes.state, nodes: nodes.state,
payouts: payouts.state, payouts: payouts.state,
bandwidth: bandwidth.state,
operators: operators.state, operators: operators.state,
}; };
this.modules = { this.modules = {
nodes, nodes,
payouts, payouts,
bandwidth,
operators, operators,
}; };
} }
@ -53,15 +64,18 @@ const nodesClient: NodesClient = new NodesClient();
const nodesService: Nodes = new Nodes(nodesClient); const nodesService: Nodes = new Nodes(nodesClient);
const payoutsClient: PayoutsClient = new PayoutsClient(); const payoutsClient: PayoutsClient = new PayoutsClient();
const payoutsService: Payouts = new Payouts(payoutsClient); const payoutsService: Payouts = new Payouts(payoutsClient);
const bandwidthClient = new BandwidthClient();
const bandwidthService = new Bandwidth(bandwidthClient);
const operatorsClient: OperatorsClient = new OperatorsClient(); const operatorsClient: OperatorsClient = new OperatorsClient();
const operatorsService: Operators = new Operators(operatorsClient); const operatorsService: Operators = new Operators(operatorsClient);
// Modules // Modules
const nodesModule: NodesModule = new NodesModule(nodesService); const nodesModule: NodesModule = new NodesModule(nodesService);
const payoutsModule: PayoutsModule = new PayoutsModule(payoutsService); const payoutsModule: PayoutsModule = new PayoutsModule(payoutsService);
const bandwidthModule: BandwidthModule = new BandwidthModule(bandwidthService);
const operatorsModule: OperatorsModule = new OperatorsModule(operatorsService); const operatorsModule: OperatorsModule = new OperatorsModule(operatorsService);
// Store // Store
export const store: Store<RootState> = new Vuex.Store<RootState>( export const store: Store<RootState> = new Vuex.Store<RootState>(
new MultinodeStoreOptions(nodesModule, payoutsModule, operatorsModule), new MultinodeStoreOptions(nodesModule, payoutsModule, operatorsModule, bandwidthModule),
); );

View File

@ -0,0 +1,177 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* ChartData class holds info for ChartData entity.
*/
export class ChartData {
public labels: string[];
public datasets: DataSets[] = [];
public constructor(labels: string[], backgroundColor: string, borderColor: string, borderWidth: number, data: number[]) {
this.labels = labels;
for (let i = 0; i < this.labels.length; i++) {
this.datasets[i] = new DataSets(backgroundColor, borderColor, borderWidth, data);
}
}
}
/**
* DiskStatChartData class holds info for Disk Stat Chart.
*/
export class DiskStatChartData {
public constructor(
public datasets: DiskStatDataSet[] = [new DiskStatDataSet()],
) {}
}
/**
* DiskStatDataSet describes all required data for disk stat chart dataset.
*/
export class DiskStatDataSet {
public constructor(
public label: string = '',
public backgroundColor: string[] = ['#D6D6D6', '#0059D0', '#8FA7C6'],
public data: number[] = [],
) {}
}
/**
* DataSets class holds info for chart's DataSets entity.
*/
class DataSets {
public backgroundColor: string;
public borderColor: string;
public borderWidth: number;
public data: number[];
public constructor(backgroundColor: string, borderColor: string, borderWidth: number, data: number[]) {
this.backgroundColor = backgroundColor;
this.borderColor = borderColor;
this.borderWidth = borderWidth;
this.data = data;
}
}
/**
* StylingConstants holds tooltip styling constants
*/
class StylingConstants {
public static tooltipOpacity = '1';
public static tooltipPosition = 'absolute';
public static pointWidth = '10px';
public static pointHeight = '10px';
public static borderRadius = '20px';
}
/**
* Styling holds tooltip's styling configuration
*/
class Styling {
public constructor(
public tooltipModel: any,
public element: HTMLElement,
public topPosition: number,
public leftPosition: number,
public chartPosition: ClientRect,
) {}
}
/**
* TooltipParams holds tooltip's configuration
*/
export class TooltipParams {
public constructor(
public tooltipModel: any,
public chartId: string,
public tooltipId: string,
public pointId: string,
public markUp: string,
public tooltipTop: number,
public tooltipLeft: number,
public pointTop: number,
public pointLeft: number,
public color: string,
) {}
}
/**
* Tooltip provides custom tooltip rendering
*/
export class Tooltip {
public static custom(params: TooltipParams): void {
const chart = document.getElementById(params.chartId);
if (!chart) {
return;
}
const tooltip: HTMLElement = Tooltip.createTooltip(params.tooltipId);
const point: HTMLElement = Tooltip.createPoint(params.pointId);
if (!params.tooltipModel.opacity) {
Tooltip.remove(tooltip, point);
return;
}
if (params.tooltipModel.body) {
Tooltip.render(tooltip, params.markUp);
}
const position = chart.getBoundingClientRect();
const tooltipStyling = new Styling(params.tooltipModel, tooltip, params.tooltipTop, params.tooltipLeft, position);
Tooltip.elemStyling(tooltipStyling);
const pointStyling = new Styling(params.tooltipModel, point, params.pointTop, params.pointLeft, position);
Tooltip.elemStyling(pointStyling);
Tooltip.pointStyling(point, params.color);
}
private static createTooltip(id: string): HTMLElement {
let tooltipEl = document.getElementById(id);
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = id;
document.body.appendChild(tooltipEl);
}
return tooltipEl;
}
private static createPoint(id: string): HTMLElement {
let tooltipPoint = document.getElementById(id);
if (!tooltipPoint) {
tooltipPoint = document.createElement('div');
tooltipPoint.id = id;
document.body.appendChild(tooltipPoint);
}
return tooltipPoint;
}
private static remove(tooltipEl: HTMLElement, tooltipPoint: HTMLElement) {
document.body.removeChild(tooltipEl);
document.body.removeChild(tooltipPoint);
}
private static render(tooltip: HTMLElement, markUp: string) {
tooltip.innerHTML = markUp;
}
private static elemStyling(elemStyling: Styling) {
elemStyling.element.style.opacity = StylingConstants.tooltipOpacity;
elemStyling.element.style.position = StylingConstants.tooltipPosition;
elemStyling.element.style.left = `${elemStyling.chartPosition.left + elemStyling.tooltipModel.caretX - elemStyling.leftPosition}px`;
elemStyling.element.style.top = `${elemStyling.chartPosition.top + window.pageYOffset + elemStyling.tooltipModel.caretY - elemStyling.topPosition}px`;
}
private static pointStyling(point: HTMLElement, color: string) {
point.style.width = StylingConstants.pointWidth;
point.style.height = StylingConstants.pointHeight;
point.style.backgroundColor = color;
point.style.borderRadius = StylingConstants.borderRadius;
}
}

View File

@ -0,0 +1,185 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { BandwidthRollup } from '@/bandwidth';
import { SizeBreakpoints } from '@/private/memory/size';
const shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
// TODO: move to diskspace package
/**
* Stamp is storage usage stamp for satellite at some point in time
*/
export class Stamp {
public atRestTotal: number;
public intervalStart: Date;
public constructor(atRestTotal: number = 0, intervalStart: Date = new Date()) {
this.atRestTotal = atRestTotal;
this.intervalStart = intervalStart;
}
/**
* Creates new empty instance of stamp with defined date
* @param date - holds specific date of the month
* @returns Stamp - new empty instance of stamp with defined date
*/
public static emptyWithDate(date: number): Stamp {
const now = new Date();
now.setUTCDate(date);
now.setUTCHours(0, 0, 0, 0);
return new Stamp(0, now);
}
}
/**
* Used to display correct and convenient data on chart.
*/
export class Chart {
/**
* Brings chart data to a more compact form.
* @param data - holds array of chart data in numeric form
* @returns data - numeric array of normalized data
*/
public static normalizeChartData(data: number[]): number[] {
const maxBytes = Math.ceil(Math.max(...data));
let divider: number = SizeBreakpoints.PB;
switch (true) {
case maxBytes < SizeBreakpoints.MB:
divider = SizeBreakpoints.KB;
break;
case maxBytes < SizeBreakpoints.GB:
divider = SizeBreakpoints.MB;
break;
case maxBytes < SizeBreakpoints.TB:
divider = SizeBreakpoints.GB;
break;
case maxBytes < SizeBreakpoints.PB:
divider = SizeBreakpoints.TB;
break;
}
return data.map(elem => elem / divider);
}
/**
* gets chart data dimension depending on data size.
* @param data - holds array of chart data in numeric form
* @returns dataDimension - string of data dimension
*/
public static getChartDataDimension(data: number[]): string {
const maxBytes = Math.ceil(Math.max(...data));
let dataDimension: string;
switch (true) {
case maxBytes < SizeBreakpoints.MB:
dataDimension = 'KB';
break;
case maxBytes < SizeBreakpoints.GB:
dataDimension = 'MB';
break;
case maxBytes < SizeBreakpoints.TB:
dataDimension = 'GB';
break;
case maxBytes < SizeBreakpoints.PB:
dataDimension = 'TB';
break;
default:
dataDimension = 'PB';
}
return dataDimension;
}
/**
* Used to display correct number of days on chart's labels.
*
* @returns daysDisplayed - array of days converted to a string by using the current or specified locale
*/
public static daysDisplayedOnChart(): string[] {
const daysDisplayed = Array<string>(new Date().getUTCDate());
const currentMonth = shortMonthNames[new Date().getUTCMonth()].toUpperCase();
for (let i = 0; i < daysDisplayed.length; i++) {
daysDisplayed[i] = `${currentMonth} ${i + 1}`;
}
if (daysDisplayed.length === 1) {
daysDisplayed.unshift('0');
}
return daysDisplayed;
}
/**
* Adds missing bandwidth usage for bandwidth chart data for each day of month.
* @param fetchedData - array of data that is spread over missing bandwidth usage for each day of the month
* @returns bandwidthChartData - array of filled data
*/
public static populateEmptyBandwidth(fetchedData: BandwidthRollup[]): BandwidthRollup[] {
const bandwidthChartData: BandwidthRollup[] = new Array(new Date().getUTCDate());
const data: BandwidthRollup[] = fetchedData || [];
if (data.length === 0) {
return bandwidthChartData;
}
outer:
for (let i = 0; i < bandwidthChartData.length; i++) {
const date = i + 1;
for (let j = 0; j < data.length; j++) {
if (data[j].intervalStart.getUTCDate() === date) {
bandwidthChartData[i] = data[j];
continue outer;
}
}
bandwidthChartData[i] = BandwidthRollup.emptyWithDate(date);
}
if (bandwidthChartData.length === 1) {
bandwidthChartData.unshift(BandwidthRollup.emptyWithDate(1));
bandwidthChartData[0].intervalStart.setUTCHours(0, 0, 0, 0);
}
return bandwidthChartData;
}
/**
* Adds missing stamps for storage chart data for each day of month.
* @param fetchedData - array of data that is spread over missing stamps for each day of the month
* @returns storageChartData - array of filled data
*/
public static populateEmptyStamps(fetchedData: Stamp[]): Stamp[] {
const storageChartData: Stamp[] = new Array(new Date().getUTCDate());
const data: Stamp[] = fetchedData || [];
if (data.length === 0) {
return storageChartData;
}
outer:
for (let i = 0; i < storageChartData.length; i++) {
const date = i + 1;
for (let j = 0; j < data.length; j++) {
if (data[j].intervalStart.getUTCDate() === date) {
storageChartData[i] = data[j];
continue outer;
}
}
storageChartData[i] = Stamp.emptyWithDate(date);
}
if (storageChartData.length === 1) {
storageChartData.unshift(Stamp.emptyWithDate(1));
storageChartData[0].intervalStart.setUTCHours(0, 0, 0, 0);
}
return storageChartData;
}
}

View File

@ -8,25 +8,97 @@
<node-selection-dropdown /> <node-selection-dropdown />
<satellite-selection-dropdown /> <satellite-selection-dropdown />
</div> </div>
<div class="chart-container">
<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"
:class="{ 'active': (!isEgressChartShown && !isIngressChartShown) }"
@click.stop="openBandwidthChart"
>
Bandwidth
</button>
<button
name="Show Egress Chart"
class="chart-container__title-area__chart-choice-item"
:class="{ 'active': isEgressChartShown }"
@click.stop="openEgressChart"
>
Egress
</button>
<button
name="Show Ingress Chart"
class="chart-container__title-area__chart-choice-item"
:class="{ 'active': isIngressChartShown }"
@click.stop="openIngressChart"
>
Ingress
</button>
</div>
</div>
<p class="chart-container__amount" v-if="isEgressChartShown"><b>{{ bandwidth.egressSummary | bytesToBase10String }}</b></p>
<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"/>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import BandwidthChart from '@/app/components/bandwidth/BandwidthChart.vue';
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 NodeSelectionDropdown from '@/app/components/common/NodeSelectionDropdown.vue';
import SatelliteSelectionDropdown from '@/app/components/common/SatelliteSelectionDropdown.vue'; import SatelliteSelectionDropdown from '@/app/components/common/SatelliteSelectionDropdown.vue';
import { UnauthorizedError } from '@/api'; import { UnauthorizedError } from '@/api';
import { BandwidthTraffic } from '@/bandwidth';
@Component({ @Component({
components: { components: {
EgressChart,
IngressChart,
BandwidthChart,
NodeSelectionDropdown, NodeSelectionDropdown,
SatelliteSelectionDropdown, SatelliteSelectionDropdown,
}, },
}) })
export default class BandwidthPage extends Vue { export default class BandwidthPage extends Vue {
public chartWidth: number = 0;
public chartHeight: number = 0;
public isEgressChartShown: boolean = false;
public isIngressChartShown: boolean = false;
public $refs: {
chart: HTMLElement;
};
public get bandwidth(): BandwidthTraffic {
return this.$store.state.bandwidth.traffic;
}
/**
* Used container size recalculation for charts resizing.
*/
public recalculateChartDimensions(): void {
this.chartWidth = this.$refs['chart'].clientWidth;
this.chartHeight = this.$refs['chart'].clientHeight;
}
/**
* Lifecycle hook after initial render.
* Adds event on window resizing to recalculate size of charts.
*/
public async mounted(): Promise<void> { public async mounted(): Promise<void> {
window.addEventListener('resize', this.recalculateChartDimensions);
try { try {
await this.$store.dispatch('nodes/fetch'); await this.$store.dispatch('nodes/fetch');
} catch (error) { } catch (error) {
@ -36,6 +108,66 @@ export default class BandwidthPage extends Vue {
// TODO: notify error // TODO: notify error
} }
await this.fetchBandwidth();
// 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();
}
});
this.recalculateChartDimensions();
}
/**
* Lifecycle hook before component destruction.
* Removes event on window resizing.
*/
public beforeDestroy(): void {
window.removeEventListener('resize', this.recalculateChartDimensions);
}
/**
* Changes bandwidth chart source to summary of ingress and egress.
*/
public openBandwidthChart(): void {
this.isEgressChartShown = false;
this.isIngressChartShown = false;
}
/**
* Changes bandwidth chart source to ingress.
*/
public openIngressChart(): void {
this.isEgressChartShown = false;
this.isIngressChartShown = true;
}
/**
* Changes bandwidth chart source to egress.
*/
public openEgressChart(): void {
this.isEgressChartShown = true;
this.isIngressChartShown = false;
}
/**
* Fetches bandwidth information.
*/
private async fetchBandwidth(): Promise<void> {
try {
await this.$store.dispatch('bandwidth/fetch');
} catch (error) {
if (error instanceof UnauthorizedError) {
// TODO: redirect to login screen.
}
// TODO: notify error
}
} }
} }
</script> </script>
@ -68,5 +200,68 @@ export default class BandwidthPage extends Vue {
max-width: unset; max-width: unset;
} }
} }
& .chart-container {
box-sizing: border-box;
width: 100%;
height: 401px;
background-color: white;
border: 1px solid var(--c-gray--light);
border-radius: 11px;
padding: 32px 30px;
margin: 20px 0 13px 0;
position: relative;
&__title-area {
display: flex;
align-items: center;
justify-content: space-between;
&__buttons-area {
display: flex;
flex-direction: row;
align-items: flex-end;
}
&__title {
font-family: 'font_regular', sans-serif;
font-size: 14px;
color: var(--c-gray);
user-select: none;
}
&__chart-choice-item {
padding: 6px 8px;
background-color: #e7e9eb;
border-radius: 6px;
font-size: 12px;
color: #586474;
max-height: 25px;
cursor: pointer;
user-select: none;
margin-left: 9px;
border: none;
&.active {
background-color: #d5d9dc;
color: #131d3a;
}
}
}
&__amount {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 57px;
color: var(--c-title);
}
&__chart {
position: absolute;
left: 0;
width: calc(100% - 10px);
height: 240px;
}
}
} }
</style> </style>

View File

@ -23,6 +23,19 @@ export class BandwidthRollup {
public deletes: number = 0, public deletes: number = 0,
public intervalStart: Date = new Date(), public intervalStart: Date = new Date(),
) {} ) {}
/**
* Creates new empty instance of used bandwidth with defined date.
* @param date - holds specific date of the month
* @returns BandwidthUsed - new empty instance of used bandwidth with defined date
*/
public static emptyWithDate(date: number): BandwidthRollup {
const now = new Date();
now.setUTCDate(date);
now.setUTCHours(0, 0, 0, 0);
return new BandwidthRollup(new Egress(0, 0, 0), new Ingress(0, 0), 0, now);
}
} }
/** /**

View File

@ -4,7 +4,7 @@
/** /**
* Base10 sizes. * Base10 sizes.
*/ */
enum SizeBreakpoints { export enum SizeBreakpoints {
KB = 1e3, KB = 1e3,
MB = 1e6, MB = 1e6,
GB = 1e9, GB = 1e9,