web/storagenode: markup and logic for node operator dashboard (#2906)
This commit is contained in:
parent
c81e4fcb9e
commit
c5658fa736
27
web/storagenode/.gitignore
vendored
Normal file
27
web/storagenode/.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
package-lock.json
|
||||
coverage
|
||||
temp
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Allow images
|
||||
!*.svg
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
8
web/storagenode/babel.config.js
Normal file
8
web/storagenode/babel.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
16
web/storagenode/index.html
Normal file
16
web/storagenode/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="apple-touch-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAL8SURBVHgBxVcxTFNRFL01JvylrRsmNWETWWSybg5I3NTQBCdRFyfrDuoKMgMyOQgdTcDAJmrUrWWCgQIbhJKy0TJQpsc73P/S9/tfP+8/GnqSn1fo77/3nnPufe8nhAR1ETeoy7hJjpj93/ycu0+UuUVuEA5Y2xaifirEVpX/nvknnBFbgpMGUfmIKOkRLRT578oxXy6IJcFCialH0EyaaPoZBy7tEQ3NEY1IKd4/iidHwqYLijLA559cuY6dT0RjBU5AAYm9fiivLFnBKMGBTyeqQ4BXhXDwdqjUiKZkskOzREsbzeeBNRMCEiDgr12uYl1WNbnW/oc2iUys8jrQyyxhHRkM3hdgAMFBHQyGG/GDqyDlsSeS/npQC99jlEBpOnyX2XCF8sGhZLbeMLMZkCDbJ1nYYTfDeMP9fMH5y5vmIKYE8RxUjBXPedDH1Zu6I9QFSzLQxErz4Xn5oNwg+2NSmuv3Lkvz4QlTi8rupDlBmA6tqQLrnYNCvoxSNAOtUEaakwzMv+ALidTP2OlKKiSK75Cs6hy9NYFkjzmG1SBCIuUq0Za8pgydge8R9E+e10qNrGE1ikH5435mo11bQgr4B9LEgVUC0Npm1o+vcuvBxB1NYFsaaeC2XUuW/Xs7msC9Xqa+MMa9jQr1KtXAQoKYHakeskbIhDrVasdTbbVY4s8ZYld/9PWuyeTSHksFBjBFcZ+aH/j/yZk5gcAcgImgIX6MNsKKhKBta1sB2A3HV5pD6iJQIzw/MICwoohc1F6ALBH03XemFYPl+VdzcBNUh6j5gZZEcP341opAAnX/AXl/A0FlrrshgMRR+YUvPPN8CHgAxlqWVYuEdH7V/ZilA6cosFDa53EcmUDKC+7X+IwxHEVhO0DK6aeXH88uHcWQA8xE7Yg69M6xgdWZUEFtNNDyx1s2KnyDIxu22zdZTjgWhANm/vL6clGIsnw3+Fbk94RreS8AMGrBxvwoT0lMPnSNC2JJoAPdgnMBJLjKq5lzAp1C19+OzwFiYzAU5f7eeQAAAABJRU5ErkJggg==" type="image/x-icon">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="description" content="Node Dashboard page">
|
||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAL8SURBVHgBxVcxTFNRFL01JvylrRsmNWETWWSybg5I3NTQBCdRFyfrDuoKMgMyOQgdTcDAJmrUrWWCgQIbhJKy0TJQpsc73P/S9/tfP+8/GnqSn1fo77/3nnPufe8nhAR1ETeoy7hJjpj93/ycu0+UuUVuEA5Y2xaifirEVpX/nvknnBFbgpMGUfmIKOkRLRT578oxXy6IJcFCialH0EyaaPoZBy7tEQ3NEY1IKd4/iidHwqYLijLA559cuY6dT0RjBU5AAYm9fiivLFnBKMGBTyeqQ4BXhXDwdqjUiKZkskOzREsbzeeBNRMCEiDgr12uYl1WNbnW/oc2iUys8jrQyyxhHRkM3hdgAMFBHQyGG/GDqyDlsSeS/npQC99jlEBpOnyX2XCF8sGhZLbeMLMZkCDbJ1nYYTfDeMP9fMH5y5vmIKYE8RxUjBXPedDH1Zu6I9QFSzLQxErz4Xn5oNwg+2NSmuv3Lkvz4QlTi8rupDlBmA6tqQLrnYNCvoxSNAOtUEaakwzMv+ALidTP2OlKKiSK75Cs6hy9NYFkjzmG1SBCIuUq0Za8pgydge8R9E+e10qNrGE1ikH5435mo11bQgr4B9LEgVUC0Npm1o+vcuvBxB1NYFsaaeC2XUuW/Xs7msC9Xqa+MMa9jQr1KtXAQoKYHakeskbIhDrVasdTbbVY4s8ZYld/9PWuyeTSHksFBjBFcZ+aH/j/yZk5gcAcgImgIX6MNsKKhKBta1sB2A3HV5pD6iJQIzw/MICwoohc1F6ALBH03XemFYPl+VdzcBNUh6j5gZZEcP341opAAnX/AXl/A0FlrrshgMRR+YUvPPN8CHgAxlqWVYuEdH7V/ZilA6cosFDa53EcmUDKC+7X+IwxHEVhO0DK6aeXH88uHcWQA8xE7Yg69M6xgdWZUEFtNNDyx1s2KnyDIxu22zdZTjgWhANm/vL6clGIsnw3+Fbk94RreS8AMGrBxvwoT0lMPnSNC2JJoAPdgnMBJLjKq5lzAp1C19+OzwFiYzAU5f7eeQAAAABJRU5ErkJggg==" type="image/x-icon">
|
||||
<title>Node Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
50
web/storagenode/package.json
Normal file
50
web/storagenode/package.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "storj-storagenode",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"lint": "vue-cli-service lint",
|
||||
"build": "vue-cli-service build",
|
||||
"debug": "vue-cli-service build --mode development"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "2.8.0",
|
||||
"vue": "2.6.10",
|
||||
"vue-chartjs": "3.4.2",
|
||||
"vue-class-component": "6.0.0",
|
||||
"vue-property-decorator": "8.2.2",
|
||||
"vue-router": "3.1.2",
|
||||
"vuex": "3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.5.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.5.0",
|
||||
"@types/sinon": "7.0.13",
|
||||
"@vue/cli-plugin-babel": "3.11.0",
|
||||
"@vue/cli-plugin-typescript": "3.11.0",
|
||||
"@vue/cli-plugin-unit-jest": "3.11.0",
|
||||
"@vue/cli-service": "3.11.0",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"node-sass": "4.12.0",
|
||||
"sass-loader": "7.1.0",
|
||||
"tslint": "5.19.0",
|
||||
"tslint-consistent-codestyle": "1.15.1",
|
||||
"tslint-loader": "3.5.4",
|
||||
"typescript": "3.5.3",
|
||||
"vue-template-compiler": "2.6.10",
|
||||
"vue-tslint": "0.3.2",
|
||||
"vue-tslint-loader": "3.5.6",
|
||||
"webpack": "4.39.3"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
40
web/storagenode/src/app/App.vue
Normal file
40
web/storagenode/src/app/App.vue
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class App extends Vue {}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
body {
|
||||
margin: 0 !important;
|
||||
font-family: 'font_regular';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: "font_regular";
|
||||
src: url("../../static/fonts/font_regular.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: "font_medium";
|
||||
src: url("../../static/fonts/font_medium.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: "font_bold";
|
||||
src: url("../../static/fonts/font_bold.ttf");
|
||||
}
|
||||
</style>
|
190
web/storagenode/src/app/components/BandwidthChart.vue
Normal file
190
web/storagenode/src/app/components/BandwidthChart.vue
Normal file
@ -0,0 +1,190 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="chart">
|
||||
<Chart
|
||||
id="bandwidth-chart"
|
||||
:chartData="chartData"
|
||||
:width="400"
|
||||
:height="200"
|
||||
:tooltipConstructor="bandwidthTooltip" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import Chart from '@/app/components/Chart.vue';
|
||||
import { ChartData } from '@/app/types/chartData';
|
||||
import { ChartUtils } from '@/app/utils/chartUtils';
|
||||
import { formatBytes } from '@/app/utils/converter';
|
||||
import { BandwidthUsed } from '@/storagenode/satellite';
|
||||
|
||||
/**
|
||||
* 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: BandwidthUsed) {
|
||||
this.normalEgress = formatBytes(bandwidth.egress.usage);
|
||||
this.normalIngress = formatBytes(bandwidth.ingress.usage);
|
||||
this.repairIngress = formatBytes(bandwidth.ingress.repair);
|
||||
this.repairEgress = formatBytes(bandwidth.egress.repair);
|
||||
this.auditEgress = formatBytes(bandwidth.egress.audit);
|
||||
this.date = bandwidth.intervalStart.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
Chart,
|
||||
},
|
||||
})
|
||||
export default class BandwidthChart extends Vue {
|
||||
private get allBandwidth(): BandwidthUsed[] {
|
||||
return ChartUtils.populateEmptyBandwidth(this.$store.state.node.bandwidthChartData);
|
||||
}
|
||||
|
||||
public get chartData(): ChartData {
|
||||
let data: number[] = [0];
|
||||
const daysCount = ChartUtils.daysDisplayedOnChart(new Date());
|
||||
const chartBackgroundColor = '#F2F6FC';
|
||||
const chartBorderColor = '#1F49A3';
|
||||
const chartBorderWidth = 2;
|
||||
|
||||
if (this.allBandwidth.length) {
|
||||
data = ChartUtils.normalizeChartData(this.allBandwidth.map((elem) => {
|
||||
return elem.summary();
|
||||
}));
|
||||
}
|
||||
|
||||
return new ChartData(daysCount, chartBackgroundColor, chartBorderColor, chartBorderWidth, data);
|
||||
}
|
||||
|
||||
public bandwidthTooltip(tooltipModel): void {
|
||||
// Tooltip Element
|
||||
let tooltipEl = document.getElementById('bandwidth-tooltip');
|
||||
// Create element on first render
|
||||
if (!tooltipEl) {
|
||||
tooltipEl = document.createElement('div');
|
||||
tooltipEl.id = 'bandwidth-tooltip';
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
|
||||
// Hide if no tooltip
|
||||
if (!tooltipModel.opacity) {
|
||||
document.body.removeChild(tooltipEl);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Set Text
|
||||
if (tooltipModel.body) {
|
||||
const dataIndex = tooltipModel.dataPoints[0].index;
|
||||
const dataPoint = new BandwidthTooltip(this.allBandwidth[dataIndex]);
|
||||
|
||||
tooltipEl.innerHTML = `<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>NORMAL</p>
|
||||
<p class='tooltip-body__info__egress-value'><b>${dataPoint.normalEgress}</b></p>
|
||||
<p class='tooltip-body__info__ingress-value'><b>${dataPoint.normalIngress}</b></p>
|
||||
</div>
|
||||
<div class='tooltip-body__info'>
|
||||
<p>REPAIR</p>
|
||||
<p class='tooltip-body__info__egress-value'><b>${dataPoint.repairEgress}</b></p>
|
||||
<p class='tooltip-body__info__ingress-value'><b>${dataPoint.repairIngress}</b></p>
|
||||
</div>
|
||||
<div class='tooltip-body__info'>
|
||||
<p>AUDIT</p>
|
||||
<p class='tooltip-body__info__egress-value'><b>${dataPoint.auditEgress}</b></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='tooltip-footer'>
|
||||
<p>${dataPoint.date}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// `this` will be the overall tooltip
|
||||
const bandwidthChart = document.getElementById('bandwidth-chart');
|
||||
if (bandwidthChart) {
|
||||
const position = bandwidthChart.getBoundingClientRect();
|
||||
tooltipEl.style.opacity = '1';
|
||||
tooltipEl.style.position = 'absolute';
|
||||
tooltipEl.style.left = position.left + tooltipModel.caretX + 'px';
|
||||
tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#bandwidth-tooltip {
|
||||
background-color: #FFFFFF;
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px #D2D6DE;
|
||||
color: #535F77;
|
||||
padding: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
padding: 0 35px 0 83px;
|
||||
line-height: 57px;
|
||||
|
||||
&__ingress {
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-body {
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
background-color: #EBECF0;
|
||||
border-radius: 12px;
|
||||
padding: 14px 17px 14px 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
|
||||
b {
|
||||
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;
|
||||
}
|
||||
</style>
|
61
web/storagenode/src/app/components/Bar.vue
Normal file
61
web/storagenode/src/app/components/Bar.vue
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="bar-container">
|
||||
<div class="bar-container__fill" :style="barFillStyle"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
/**
|
||||
* BarFillStyle class holds info for BarFillStyle entity.
|
||||
*/
|
||||
class BarFillStyle {
|
||||
'background-color': string;
|
||||
width: string;
|
||||
|
||||
public constructor(backgroundColor: string, width: string) {
|
||||
this['background-color'] = backgroundColor;
|
||||
this.width = width;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class Bar extends Vue {
|
||||
@Prop({default: ''})
|
||||
private readonly current: string;
|
||||
@Prop({default: ''})
|
||||
private readonly max: string;
|
||||
@Prop({default: '#224CA5'})
|
||||
private readonly color: string;
|
||||
|
||||
public get barFillStyle(): BarFillStyle {
|
||||
const width = (parseFloat(this.current) / parseFloat(this.max)) * 100 + '%';
|
||||
|
||||
return new BarFillStyle(this.color, width);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bar-container {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
margin-top: 11px;
|
||||
border-radius: 4px;
|
||||
background-color: #F4F6F9;
|
||||
position: relative;
|
||||
|
||||
&__fill {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
73
web/storagenode/src/app/components/BarInfoContainer.vue
Normal file
73
web/storagenode/src/app/components/BarInfoContainer.vue
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="remaining-space-container">
|
||||
<p class="remaining-space-container__title">{{label}}</p>
|
||||
<p class="remaining-space-container__amount"><b>{{remaining}}</b>GB</p>
|
||||
<div class="remaining-space-container__bar">
|
||||
<InfoComponent :text="infoMessage">
|
||||
<Bar :current="currentBarAmount" :max="maxBarAmount" color="#224CA5"/>
|
||||
</InfoComponent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import Bar from '@/app/components/Bar.vue';
|
||||
import InfoComponent from '@/app/components/InfoComponent.vue';
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
Bar,
|
||||
InfoComponent,
|
||||
},
|
||||
})
|
||||
export default class BarInfoContainer extends Vue {
|
||||
@Prop({default: ''})
|
||||
private readonly label: string;
|
||||
@Prop({default: ''})
|
||||
private readonly amount: number;
|
||||
@Prop({default: ''})
|
||||
private readonly infoText: string;
|
||||
@Prop({default: ''})
|
||||
private readonly currentBarAmount: number;
|
||||
@Prop({default: ''})
|
||||
private readonly maxBarAmount: number;
|
||||
|
||||
public get infoMessage(): string {
|
||||
return `${Math.floor(100 - (this.currentBarAmount / this.maxBarAmount) * 100)}% ${this.infoText}`;
|
||||
}
|
||||
|
||||
public get remaining(): string {
|
||||
return this.amount.toFixed(2);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.remaining-space-container {
|
||||
width: 325px;
|
||||
height: 90px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E9EFF4;
|
||||
border-radius: 11px;
|
||||
padding: 34px 36px 39px 39px;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
color: #586C86;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 32px;
|
||||
font-family: 'font_bold';
|
||||
line-height: 57px;
|
||||
color: #535F77;
|
||||
}
|
||||
}
|
||||
</style>
|
88
web/storagenode/src/app/components/Chart.vue
Normal file
88
web/storagenode/src/app/components/Chart.vue
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<script lang="ts">
|
||||
import * as VChart from 'vue-chartjs';
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
|
||||
import { ChartData } from '@/app/types/chartData';
|
||||
|
||||
@Component({
|
||||
extends: VChart.Line
|
||||
})
|
||||
export default class Chart 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 {
|
||||
return {
|
||||
responsive: false,
|
||||
maintainAspectRatios: false,
|
||||
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
hitRadius: 5,
|
||||
hoverRadius: 5,
|
||||
hoverBackgroundColor: '#4D72B7',
|
||||
}
|
||||
},
|
||||
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: false,
|
||||
}],
|
||||
xAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
fontFamily: 'font_regular',
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
},
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
|
||||
custom: ((tooltipModel) => {
|
||||
this.tooltipConstructor(tooltipModel);
|
||||
}),
|
||||
|
||||
labels: {
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
89
web/storagenode/src/app/components/ChecksAreaContainer.vue
Normal file
89
web/storagenode/src/app/components/ChecksAreaContainer.vue
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="checks-area-container">
|
||||
<div class="checks-area-container__header">
|
||||
<p class="checks-area-container__header__title">{{label}}</p>
|
||||
<InfoComponent :text="infoText" isExtraPadding="true" isCustomPosition="true">
|
||||
<div>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" alt="info image">
|
||||
<rect width="18" height="18" rx="9" fill="#5A667C"/>
|
||||
<path d="M8.99928 8.00325C8.44956 8.00325 7.99979 8.48247 7.99979 9.06819L7.99979 13.3351C7.99979 13.3883 8.00312 13.4451 8.00978 13.4984C8.08308 14.006 8.49953 14.4 8.99928 14.4C9.54901 14.4 9.99878 13.9208 9.99878 13.3351L9.99878 9.07174C9.99878 8.48247 9.54901 8.00325 8.99928 8.00325Z" fill="white"/>
|
||||
<path d="M8.99988 6.96423C9.77415 6.96423 10.3992 6.33921 10.3992 5.56494C10.3992 4.79066 9.77415 4.16564 8.99988 4.16564C8.22561 4.16564 7.60059 4.79066 7.60059 5.56494C7.59748 6.33921 8.2225 6.96423 8.99988 6.96423Z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
</InfoComponent>
|
||||
</div>
|
||||
<p class="checks-area-container__amount"><b>{{value}}</b>%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import InfoComponent from '@/app/components/InfoComponent.vue';
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
InfoComponent,
|
||||
},
|
||||
})
|
||||
export default class ChecksAreaContainer extends Vue {
|
||||
@Prop({default: ''})
|
||||
private readonly label: string;
|
||||
@Prop({default: ''})
|
||||
private readonly amount: number;
|
||||
@Prop({default: ''})
|
||||
private readonly infoText: string;
|
||||
|
||||
public get value(): string {
|
||||
return this.amount.toFixed(1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.checks-area-container {
|
||||
width: 325px;
|
||||
height: 70px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E9EFF4;
|
||||
border-radius: 11px;
|
||||
padding: 34px 36px 39px 39px;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
color: #586C86;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 3px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
rect {
|
||||
fill: #A5C7EF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 32px;
|
||||
line-height: 57px;
|
||||
color: #535F77;
|
||||
}
|
||||
}
|
||||
</style>
|
135
web/storagenode/src/app/components/DiskSpaceChart.vue
Normal file
135
web/storagenode/src/app/components/DiskSpaceChart.vue
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="chart">
|
||||
<Chart
|
||||
id="disk-space-chart"
|
||||
:chartData="chartData"
|
||||
:width="400"
|
||||
:height="200"
|
||||
:tooltipConstructor="diskSpaceTooltip" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import Chart from '@/app/components/Chart.vue';
|
||||
import { ChartData } from '@/app/types/chartData';
|
||||
import { ChartUtils } from '@/app/utils/chartUtils';
|
||||
import { formatBytes } from '@/app/utils/converter';
|
||||
import { Stamp } from '@/storagenode/satellite';
|
||||
|
||||
/**
|
||||
* stores stamp data for disc space chart's tooltip
|
||||
*/
|
||||
class StampTooltip {
|
||||
public atRestTotal: string;
|
||||
public intervalStart: string;
|
||||
|
||||
public constructor(stamp: Stamp) {
|
||||
this.atRestTotal = formatBytes(stamp.atRestTotal);
|
||||
this.intervalStart = stamp.intervalStart.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
Chart,
|
||||
},
|
||||
})
|
||||
export default class DiskSpaceChart extends Vue {
|
||||
private get allStamps(): Stamp[] {
|
||||
return ChartUtils.populateEmptyStamps(this.$store.state.node.storageChartData);
|
||||
}
|
||||
|
||||
public get chartData(): ChartData {
|
||||
let data: number[] = [0];
|
||||
const daysCount = ChartUtils.daysDisplayedOnChart(new Date());
|
||||
const chartBackgroundColor = '#F2F6FC';
|
||||
const chartBorderColor = '#1F49A3';
|
||||
const chartBorderWidth = 2;
|
||||
|
||||
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 {
|
||||
// Tooltip Element
|
||||
let tooltipEl = document.getElementById('disk-space-tooltip');
|
||||
// Create element on first render
|
||||
if (!tooltipEl) {
|
||||
tooltipEl = document.createElement('div');
|
||||
tooltipEl.id = 'disk-space-tooltip';
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
|
||||
// Hide if no tooltip
|
||||
if (!tooltipModel.opacity) {
|
||||
document.body.removeChild(tooltipEl);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Set Text
|
||||
if (tooltipModel.body) {
|
||||
const dataIndex = tooltipModel.dataPoints[0].index;
|
||||
const dataPoint = new StampTooltip(this.allStamps[dataIndex]);
|
||||
|
||||
tooltipEl.innerHTML = `<div class='tooltip-body'>
|
||||
<p class='tooltip-body__data'><b>${dataPoint.atRestTotal}</b></p>
|
||||
<p class='tooltip-body__footer'>${dataPoint.intervalStart}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const diskSpaceChart = document.getElementById('disk-space-chart');
|
||||
|
||||
if (diskSpaceChart) {
|
||||
const position = diskSpaceChart.getBoundingClientRect();
|
||||
tooltipEl.style.opacity = '1';
|
||||
tooltipEl.style.position = 'absolute';
|
||||
tooltipEl.style.right = position.left + window.pageXOffset - tooltipModel.caretX - 20 + 'px';
|
||||
tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#disk-space-tooltip {
|
||||
background-color: #FFFFFF;
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px #D2D6DE;
|
||||
color: #535F77;
|
||||
padding: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-body {
|
||||
|
||||
&__data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 11px 44px 11px 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
font-size: 12px;
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
117
web/storagenode/src/app/components/InfoComponent.vue
Normal file
117
web/storagenode/src/app/components/InfoComponent.vue
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="info" @mouseenter="toggleVisibility" @mouseleave="toggleVisibility">
|
||||
<slot class="slot"></slot>
|
||||
<div class="info__message-box" v-if="isVisible" :style="messageBoxStyle" :class="{extraPadding: isExtraPadding, customPosition: isCustomPosition}">
|
||||
<div class="info__message-box__text">
|
||||
<p class="info__message-box__text__regular-text">{{text}}</p>
|
||||
<p class="info__message-box__text__bold-text">{{boldText}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
declare interface MessageBoxStyle {
|
||||
bottom: string;
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class InfoComponent extends Vue {
|
||||
private isVisible: boolean = false;
|
||||
private height: string = '5px';
|
||||
|
||||
@Prop({default: ''})
|
||||
private readonly text: string;
|
||||
@Prop({default: ''})
|
||||
private readonly boldText: string;
|
||||
@Prop({default: false})
|
||||
private readonly isExtraPadding: boolean;
|
||||
@Prop({default: false})
|
||||
private readonly isCustomPosition: boolean;
|
||||
|
||||
public toggleVisibility(): void {
|
||||
this.isVisible = !this.isVisible;
|
||||
}
|
||||
|
||||
public get messageBoxStyle(): MessageBoxStyle {
|
||||
return { bottom: this.height };
|
||||
}
|
||||
|
||||
public mounted(): void {
|
||||
const infoComponent = document.querySelector('.info');
|
||||
if (!infoComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slots = this.$slots.default;
|
||||
if (!slots) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slot = slots[0];
|
||||
if (slot && slot.elm) {
|
||||
this.height = (slot.elm as HTMLElement).offsetHeight + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.info {
|
||||
position: relative;
|
||||
|
||||
&__message-box {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
height: auto;
|
||||
width: auto;
|
||||
min-width: 210px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-image: url('../../../static/images/Message.png');
|
||||
background-size:100% 100%;
|
||||
z-index: 101;
|
||||
padding: 11px 18px 20px 18px;
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__bold-text {
|
||||
color: #586C86;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-family: 'font_bold';
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
&__regular-text {
|
||||
color: #5A6E87;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extraPadding {
|
||||
padding: 11px 18px 31px 18px;
|
||||
}
|
||||
|
||||
.customPosition {
|
||||
left: 40%;
|
||||
}
|
||||
</style>
|
89
web/storagenode/src/app/components/PayoutContainer.vue
Normal file
89
web/storagenode/src/app/components/PayoutContainer.vue
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="payout-container">
|
||||
<svg width="40" height="35" viewBox="0 0 40 35" fill="none" xmlns="http://www.w3.org/2000/svg" alt="wallet image">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 7C2.57391 7.33667 3.23903 7.53563 3.94595 7.53563C14.9031 7.52607 27.1226 7.53563 36.5405 7.53563C37.3653 7.53563 38 8.17454 38 9.00473V13.9017H23.8919C22.5616 13.9017 21.4595 15.0112 21.4595 16.3502V24.1854C21.4595 25.5244 22.5616 26.6339 23.8919 26.6339H38V31.5309C38 32.3611 37.3653 33 36.5405 33H3.45946C2.63472 33 2 32.3611 2 31.5309V7Z" fill="#A5C7EF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.90244 0C1.75688 0 0 1.75078 0 3.88889V31.5972C0 33.4657 1.53961 35 3.41463 35H36.5854C38.4604 35 40 33.4657 40 31.5972V9.23611C40 7.3676 38.4604 5.83333 36.5854 5.83333H34.1463V0.972222C34.1463 0.463313 33.6814 0 33.1707 0H3.90244ZM3.90244 1.94444H32.1951V5.83333H3.90244C2.80488 5.83333 1.95122 4.98264 1.95122 3.88889C1.95122 2.79514 2.80488 1.94444 3.90244 1.94444ZM3.90244 7.77768C3.19361 7.77768 2.52668 7.58017 1.95122 7.24597V31.5971C1.95122 32.4212 2.58766 33.0555 3.41463 33.0555H36.5854C37.4123 33.0555 38.0488 32.4212 38.0488 31.5971V26.736H23.9024C22.5686 26.736 21.4634 25.6347 21.4634 24.3055V16.5277C21.4634 15.1985 22.5686 14.0971 23.9024 14.0971H38.0488V9.23601C38.0488 8.41191 37.4123 7.77768 36.5854 7.77768C33.5841 7.77768 30.2991 7.77672 26.8612 7.77572C19.4816 7.77356 11.3975 7.7712 3.90244 7.77768ZM23.9024 16.0417H38.0488V24.7917H23.9024C23.6147 24.7917 23.4146 24.5923 23.4146 24.3056V16.5278C23.4146 16.241 23.6147 16.0417 23.9024 16.0417ZM26.8293 20.4167C26.8293 19.3419 27.702 18.4722 28.7805 18.4722C29.859 18.4722 30.7317 19.3419 30.7317 20.4167C30.7317 21.4914 29.859 22.3611 28.7805 22.3611C27.702 22.3611 26.8293 21.4914 26.8293 20.4167Z" fill="#224CA5"/>
|
||||
</svg>
|
||||
<div class="payout-container__wallet-adress-section">
|
||||
<p>{{label}}</p>
|
||||
<p><b>{{walletAddress}}</b></p>
|
||||
</div>
|
||||
<a :href="'https://etherscan.io/address/' + walletAddress" target="_blank" rel="noopener">
|
||||
<div class="payout-container__button"><b>View on Etherscan</b></div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class PayoutContainer extends Vue {
|
||||
@Prop({default: ''})
|
||||
private readonly label: string;
|
||||
@Prop({default: ''})
|
||||
private readonly walletAddress: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.payout-container {
|
||||
background-color: #FFFFFF;
|
||||
padding: 45px 32px 46px 40px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
border: 1px solid #EAEAEA;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
&__wallet-adress-section {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #586C86;
|
||||
}
|
||||
|
||||
b {
|
||||
font-size: 18px;
|
||||
color: #535F77;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
font-size: 14px;
|
||||
width: 168px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F4F6F9;
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 12px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
right: 32px;
|
||||
color: #535F77;
|
||||
|
||||
&:hover {
|
||||
background-color: #4D72B7;
|
||||
cursor: pointer;
|
||||
|
||||
b {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
199
web/storagenode/src/app/components/SNOContentFilling.vue
Normal file
199
web/storagenode/src/app/components/SNOContentFilling.vue
Normal file
@ -0,0 +1,199 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="info-area">
|
||||
<SatelliteSelectionContainer />
|
||||
<div v-if="selectedSatellite.id && selectedSatellite.disqualified" class="info-area__disqualified-info">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" alt="disqualified image">
|
||||
<path d="M37.0279 30.9608C36.5357 30.0515 36.0404 29.1405 35.5467 28.2265C34.6279 26.5312 33.7108 24.8358 32.7936 23.1405C31.692 21.1092 30.5936 19.0749 29.4936 17.0437C28.4311 15.0828 27.3717 13.1249 26.3092 11.1657C25.528 9.72504 24.7498 8.28289 23.9686 6.84088C23.7576 6.45026 23.5467 6.05964 23.3358 5.67212C23.117 5.26588 22.8858 4.87525 22.5545 4.54401C21.3983 3.37993 19.4795 3.15648 18.0889 4.0362C17.492 4.41433 17.0608 4.95028 16.7296 5.56432C16.2218 6.50184 15.7139 7.43933 15.2061 8.37996C14.2811 10.0909 13.3546 11.8018 12.4296 13.5128C11.3155 15.555 10.2108 17.602 9.10144 19.6488C8.05144 21.5894 6.99988 23.5269 5.94832 25.4692C5.17956 26.891 4.40924 28.3098 3.63896 29.7316C3.43584 30.1066 3.23272 30.4816 3.0296 30.8566C2.74523 31.3847 2.5218 31.919 2.45148 32.5284C2.25305 34.2503 3.45928 35.9472 5.12648 36.3691C5.56712 36.4816 6.00148 36.4863 6.44681 36.4863H33.9468H33.9906C34.8968 36.4675 35.7562 36.1269 36.4202 35.5097C37.0609 34.916 37.4359 34.1035 37.5421 33.2441C37.6437 32.4347 37.4093 31.6691 37.028 30.9613L37.0279 30.9608ZM18.4371 13.9528C18.4371 13.0778 19.1528 12.4294 19.9996 12.3904C20.8434 12.3513 21.5621 13.1372 21.5621 13.9528V24.956C21.5621 25.831 20.8464 26.4795 19.9996 26.5185C19.1558 26.5576 18.4371 25.7716 18.4371 24.956V13.9528ZM19.9996 31.8404C19.1215 31.8404 18.409 31.1295 18.409 30.2498C18.409 29.3717 19.1199 28.6592 19.9996 28.6592C20.8777 28.6592 21.5902 29.3701 21.5902 30.2498C21.5902 31.1279 20.8778 31.8404 19.9996 31.8404Z" fill="#F4D638"/>
|
||||
</svg>
|
||||
<p>Your node has been disqualified on <b>{{selectedSatellite.disqualified.toUTCString()}}</b>. If you have any questions regarding this please contact our <a>support</a>.</p>
|
||||
</div>
|
||||
<div v-else-if="disqualifiedSatellites.length > 0" class="info-area__disqualified-info">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" alt="disqualified image">
|
||||
<path d="M37.0279 30.9608C36.5357 30.0515 36.0404 29.1405 35.5467 28.2265C34.6279 26.5312 33.7108 24.8358 32.7936 23.1405C31.692 21.1092 30.5936 19.0749 29.4936 17.0437C28.4311 15.0828 27.3717 13.1249 26.3092 11.1657C25.528 9.72504 24.7498 8.28289 23.9686 6.84088C23.7576 6.45026 23.5467 6.05964 23.3358 5.67212C23.117 5.26588 22.8858 4.87525 22.5545 4.54401C21.3983 3.37993 19.4795 3.15648 18.0889 4.0362C17.492 4.41433 17.0608 4.95028 16.7296 5.56432C16.2218 6.50184 15.7139 7.43933 15.2061 8.37996C14.2811 10.0909 13.3546 11.8018 12.4296 13.5128C11.3155 15.555 10.2108 17.602 9.10144 19.6488C8.05144 21.5894 6.99988 23.5269 5.94832 25.4692C5.17956 26.891 4.40924 28.3098 3.63896 29.7316C3.43584 30.1066 3.23272 30.4816 3.0296 30.8566C2.74523 31.3847 2.5218 31.919 2.45148 32.5284C2.25305 34.2503 3.45928 35.9472 5.12648 36.3691C5.56712 36.4816 6.00148 36.4863 6.44681 36.4863H33.9468H33.9906C34.8968 36.4675 35.7562 36.1269 36.4202 35.5097C37.0609 34.916 37.4359 34.1035 37.5421 33.2441C37.6437 32.4347 37.4093 31.6691 37.028 30.9613L37.0279 30.9608ZM18.4371 13.9528C18.4371 13.0778 19.1528 12.4294 19.9996 12.3904C20.8434 12.3513 21.5621 13.1372 21.5621 13.9528V24.956C21.5621 25.831 20.8464 26.4795 19.9996 26.5185C19.1558 26.5576 18.4371 25.7716 18.4371 24.956V13.9528ZM19.9996 31.8404C19.1215 31.8404 18.409 31.1295 18.409 30.2498C18.409 29.3717 19.1199 28.6592 19.9996 28.6592C20.8777 28.6592 21.5902 29.3701 21.5902 30.2498C21.5902 31.1279 20.8778 31.8404 19.9996 31.8404Z" fill="#F4D638"/>
|
||||
</svg>
|
||||
<p>Your node has been disqualified on<span v-for="disqualified in disqualifiedSatellites"><b> {{disqualified.id}}</b></span>. If you have any questions regarding this please contact our <a>support</a>.</p>
|
||||
</div>
|
||||
<p class="info-area__title">Utilization & Remaining</p>
|
||||
<div class="info-area__chart-area">
|
||||
<div class="chart-container">
|
||||
<p class="chart-container__title">Bandwidth Used This Month</p>
|
||||
<p class="chart-container__amount"><b>{{bandwidthSummary}}</b></p>
|
||||
<div class="chart-container__chart">
|
||||
<BandwidthChart />
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<p class="chart-container__title">Disk Space Used This Month</p>
|
||||
<p class="chart-container__amount"><b>{{storageSummary}}*h</b></p>
|
||||
<div class="chart-container__chart">
|
||||
<DiskSpaceChart />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedSatellite.id">
|
||||
<p class="info-area__title">Uptime & Audit Checks by Satellite</p>
|
||||
<div class="info-area__checks-area">
|
||||
<ChecksAreaContainer label="Uptime Checks" :amount="checks.uptime" infoText="Uptime checks occur to make sure your node is still online. This is the percentage of uptime checks you’ve passed."/>
|
||||
<ChecksAreaContainer label="Audit Checks" :amount="checks.audit" infoText="Percentage of successful pings/communication between the node & satellite."/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="info-area__title">Remaining on the Node</p>
|
||||
<div class="info-area__remaining-space-area">
|
||||
<BarInfoContainer label="Bandwidth Remaining" :amount="bandwidth.remaining"
|
||||
infoText="of bandwidth left" :currentBarAmount="bandwidth.used" :maxBarAmount="bandwidth.available" />
|
||||
<BarInfoContainer label="Disk Space Remaining" :amount="diskSpace.remaining"
|
||||
infoText="of disk space left" :currentBarAmount="diskSpace.used" :maxBarAmount="diskSpace.available" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="info-area__title">Payout</p>
|
||||
<PayoutContainer label="STORJ Wallet Address" :walletAddress="wallet" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import BandwidthChart from '@/app/components/BandwidthChart.vue';
|
||||
import BarInfoContainer from '@/app/components/BarInfoContainer.vue';
|
||||
import ChecksAreaContainer from '@/app/components/ChecksAreaContainer.vue';
|
||||
import DiskSpaceChart from '@/app/components/DiskSpaceChart.vue';
|
||||
import PayoutContainer from '@/app/components/PayoutContainer.vue';
|
||||
import SatelliteSelectionContainer from '@/app/components/SatelliteSelectionContainer.vue';
|
||||
import { formatBytes } from '@/app/utils/converter';
|
||||
import { BandwidthInfo, DiskSpaceInfo, SatelliteInfo } from '@/storagenode/dashboard';
|
||||
|
||||
/**
|
||||
* Checks class holds info for Checks entity.
|
||||
*/
|
||||
class Checks {
|
||||
public uptime: number;
|
||||
public audit: number;
|
||||
|
||||
public constructor(uptime: number, audit: number) {
|
||||
this.uptime = uptime;
|
||||
this.audit = audit;
|
||||
}
|
||||
}
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
SatelliteSelectionContainer,
|
||||
BandwidthChart,
|
||||
DiskSpaceChart,
|
||||
BarInfoContainer,
|
||||
ChecksAreaContainer,
|
||||
PayoutContainer,
|
||||
},
|
||||
})
|
||||
export default class SNOContentFilling extends Vue {
|
||||
public get wallet(): string {
|
||||
return this.$store.state.node.info.wallet;
|
||||
}
|
||||
|
||||
public get bandwidthSummary(): string {
|
||||
return formatBytes(this.$store.state.node.bandwidthSummary);
|
||||
}
|
||||
|
||||
public get storageSummary(): string {
|
||||
return formatBytes(this.$store.state.node.storageSummary);
|
||||
}
|
||||
|
||||
public get bandwidth(): BandwidthInfo {
|
||||
return this.$store.state.node.utilization.bandwidth;
|
||||
}
|
||||
|
||||
public get diskSpace(): DiskSpaceInfo {
|
||||
return this.$store.state.node.utilization.diskSpace;
|
||||
}
|
||||
|
||||
public get checks(): Checks {
|
||||
return this.$store.state.node.checks;
|
||||
}
|
||||
|
||||
public get selectedSatellite(): SatelliteInfo {
|
||||
return this.$store.state.node.selectedSatellite;
|
||||
}
|
||||
|
||||
public get disqualifiedSatellites(): SatelliteInfo[] {
|
||||
return this.$store.state.node.disqualifiedSatellites;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
p {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.info-area {
|
||||
width: 100%;
|
||||
padding: 0 0 30px 0;
|
||||
|
||||
&__disqualified-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 27px 20px 25px;
|
||||
background-color: #FCF8E3;
|
||||
border-radius: 12px;
|
||||
width: calc(100% - 52px);
|
||||
margin-top: 17px;
|
||||
|
||||
svg {
|
||||
margin-right: 17px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
line-height: 57px;
|
||||
color: #535F77;
|
||||
}
|
||||
|
||||
&__chart-area,
|
||||
&__remaining-space-area,
|
||||
&__checks-area {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 325px;
|
||||
height: 257px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E9EFF4;
|
||||
border-radius: 11px;
|
||||
padding: 34px 36px 39px 39px;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
color: #586C86;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 32px;
|
||||
line-height: 57px;
|
||||
color: #535F77;
|
||||
}
|
||||
|
||||
&__chart {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
158
web/storagenode/src/app/components/SNOContentTitle.vue
Normal file
158
web/storagenode/src/app/components/SNOContentTitle.vue
Normal file
@ -0,0 +1,158 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="title">
|
||||
<div class="title__name">
|
||||
<h1>Your Storage Node Stats</h1>
|
||||
<p class="title__name__info">Current period: <b>{{currentMonth}}</b></p>
|
||||
</div>
|
||||
<div class="title__info">
|
||||
<svg class="check-svg" v-if="online" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" alt="online status image">
|
||||
<path d="M9 0.5C13.6942 0.5 17.5 4.3058 17.5 9C17.5 13.6942 13.6942 17.5 9 17.5C4.3058 17.5 0.5 13.6942 0.5 9C0.5 4.3058 4.3058 0.5 9 0.5Z" fill="#00CE7D" stroke="#F4F6F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.35717 9.90354C3.30671 8.7687 5.03287 7.1697 6.08406 8.30604L7.78632 10.144L11.8784 5.31912C12.8797 4.13577 14.6803 5.66083 13.6792 6.84279L8.7531 12.6514C8.28834 13.1977 7.4706 13.2659 6.96364 12.7182L4.35717 9.90354Z" fill="#F4F6F9"/>
|
||||
</svg>
|
||||
<svg class="check-svg" v-else width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" alt="offline status image">
|
||||
<path d="M9 0.5C13.6942 0.5 17.5 4.3058 17.5 9C17.5 13.6942 13.6942 17.5 9 17.5C4.3058 17.5 0.5 13.6942 0.5 9C0.5 4.3058 4.3058 0.5 9 0.5Z" fill="#E62929" stroke="#F4F6F9"/>
|
||||
<path d="M11 7L7 11M7 7L11 11" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg width="27" height="20" viewBox="0 0 27 20" fill="none" xmlns="http://www.w3.org/2000/svg" alt="status image">
|
||||
<path d="M1.0896 11.5265V9.95801H25.9184V11.5265H1.0896Z" fill="#535F77"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.681 14.1731V10.792C26.681 10.4148 26.5786 10.0448 26.3855 9.72357L22.539 3.34291C21.5179 1.64753 19.7154 0.615967 17.7755 0.615967H8.60176C6.51114 0.615967 4.59434 1.81197 3.62896 3.71888L0.549768 9.80582C0.403324 10.0939 0.326904 10.4152 0.326904 10.7422V14.1731C0.326904 17.3561 2.83589 19.9361 5.93124 19.9361H21.0767C24.172 19.9361 26.681 17.3561 26.681 14.1731ZM25.0886 10.5492C25.1323 10.622 25.1557 10.7064 25.1557 10.792V14.1731C25.1557 16.4898 23.3296 18.3676 21.0767 18.3676H5.93124C3.6783 18.3676 1.85222 16.4898 1.85222 14.1731V10.7422C1.85222 10.6677 1.86933 10.5958 1.90222 10.5311L4.98203 4.44295C5.68465 3.05506 7.07991 2.18449 8.60176 2.18449H17.7755C19.1875 2.18449 20.4993 2.93527 21.2424 4.16907L25.0886 10.5492Z" fill="#535F77"/>
|
||||
<path d="M22.3542 14.4712C22.7754 14.4712 23.1169 14.8223 23.1169 15.2555C23.1169 15.6886 22.7754 16.0397 22.3542 16.0397H17.9223C17.5011 16.0397 17.1597 15.6886 17.1597 15.2555C17.1597 14.8223 17.5011 14.4712 17.9223 14.4712H22.3542Z" fill="#535F77"/>
|
||||
</svg>
|
||||
<p class="online-status"><b>{{info.status}}</b></p>
|
||||
<p><b>Node Version</b></p>
|
||||
<p class="version">{{version}}</p>
|
||||
<InfoComponent v-if="info.isLastVersion" text="Running the minimal allowed version:" boldText="v.0.0.0" isCustomPosition="true">
|
||||
<div class="version-svg-container">
|
||||
<svg class="version-svg" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" alt="version status image">
|
||||
<path d="M9 0.5C13.6942 0.5 17.5 4.3058 17.5 9C17.5 13.6942 13.6942 17.5 9 17.5C4.3058 17.5 0.5 13.6942 0.5 9C0.5 4.3058 4.3058 0.5 9 0.5Z" fill="#00CE7D" stroke="#F4F6F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.35717 9.90354C3.30671 8.7687 5.03287 7.1697 6.08406 8.30604L7.78632 10.144L11.8784 5.31912C12.8797 4.13577 14.6803 5.66083 13.6792 6.84279L8.7531 12.6514C8.28834 13.1977 7.4706 13.2659 6.96364 12.7182L4.35717 9.90354Z" fill="#F4F6F9"/>
|
||||
</svg>
|
||||
</div>
|
||||
</InfoComponent>
|
||||
<InfoComponent v-else text="Your node is outdated. Please update to:" boldText="v.0.0.0" isCustomPosition="true">
|
||||
<div class="version-svg-container">
|
||||
<svg class="version-svg" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" alt="version status image">
|
||||
<path d="M9 0.5C13.6942 0.5 17.5 4.3058 17.5 9C17.5 13.6942 13.6942 17.5 9 17.5C4.3058 17.5 0.5 13.6942 0.5 9C0.5 4.3058 4.3058 0.5 9 0.5Z" fill="#E62929" stroke="#F4F6F9"/>
|
||||
<path d="M11 7L7 11M7 7L11 11" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</InfoComponent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import InfoComponent from '@/app/components/InfoComponent.vue';
|
||||
import { StatusOnline } from '@/app/store/modules/node';
|
||||
|
||||
/**
|
||||
* NodeInfo class holds info for NodeInfo entity.
|
||||
*/
|
||||
class NodeInfo {
|
||||
public id: string;
|
||||
public status: string;
|
||||
public version: string;
|
||||
public wallet: string;
|
||||
public isLastVersion: boolean;
|
||||
|
||||
public constructor(id: string, status: string, version: string, wallet: string, isLastVersion: boolean) {
|
||||
this.id = id;
|
||||
this.status = status;
|
||||
this.version = version;
|
||||
this.wallet = wallet;
|
||||
this.isLastVersion = isLastVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
InfoComponent,
|
||||
},
|
||||
})
|
||||
export default class SNOContentTitle extends Vue {
|
||||
public get info(): NodeInfo {
|
||||
return this.$store.state.node.info;
|
||||
}
|
||||
|
||||
public get version(): string {
|
||||
const version = this.$store.state.node.info.version;
|
||||
|
||||
return `v${version.major}.${version.minor}.${version.patch}`;
|
||||
}
|
||||
|
||||
public get online(): boolean {
|
||||
return this.$store.state.node.info.status === StatusOnline;
|
||||
}
|
||||
|
||||
public get currentMonth(): string {
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
const date = new Date();
|
||||
|
||||
return monthNames[date.getMonth()];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0 12px 0;
|
||||
color: #535F77;
|
||||
|
||||
&__name {
|
||||
margin-right: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-left: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
|
||||
.online-status {
|
||||
margin: 0 20px 0 5px;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.check-svg {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.version-svg-container {
|
||||
max-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.version-svg:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
53
web/storagenode/src/app/components/SNOFooter.vue
Normal file
53
web/storagenode/src/app/components/SNOFooter.vue
Normal file
File diff suppressed because one or more lines are too long
97
web/storagenode/src/app/components/SNOHeader.vue
Normal file
97
web/storagenode/src/app/components/SNOHeader.vue
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,74 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="satellite-selection-toggle-container" v-if="satellites" @click="toggleDropDown">
|
||||
<p><b>Choose your satellite: </b>{{selectedSatellite ? selectedSatellite : 'All satellites'}}</p>
|
||||
<svg width="8" height="4" viewBox="0 0 8 4" fill="none" xmlns="http://www.w3.org/2000/svg" alt="arrow image">
|
||||
<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="#535F77"/>
|
||||
</svg>
|
||||
<SatelliteSelectionDropdown v-if="isPopupShown"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
|
||||
import { SatelliteInfo } from '@/storagenode/dashboard';
|
||||
|
||||
import SatelliteSelectionDropdown from './SatelliteSelectionDropdown.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
SatelliteSelectionDropdown,
|
||||
},
|
||||
})
|
||||
export default class SatelliteSelectionContainer extends Vue {
|
||||
@Prop({default: ''})
|
||||
private readonly label: string;
|
||||
|
||||
public toggleDropDown(): void {
|
||||
this.$store.dispatch(APPSTATE_ACTIONS.TOGGLE_SATELLITE_SELECTION);
|
||||
}
|
||||
|
||||
public get satellites(): SatelliteInfo[] {
|
||||
return this.$store.state.node.satellites;
|
||||
}
|
||||
|
||||
public get selectedSatellite(): string {
|
||||
return this.$store.state.node.selectedSatellite.id;
|
||||
}
|
||||
|
||||
public get isPopupShown(): boolean {
|
||||
return this.$store.state.appStateModule.isSatelliteSelectionShown;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.satellite-selection-toggle-container {
|
||||
width: calc(100%-28px);
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 12px;
|
||||
padding: 0 14px 0 14px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #535F77;
|
||||
|
||||
b {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,118 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="satellite-selection-choice-container" id="satelliteDropdown">
|
||||
<div class="satellite-selection-overflow-container">
|
||||
<!-- loop for rendering satellites -->
|
||||
<div class="satellite-selection-overflow-container__satellite-choice"
|
||||
v-for="satellite in satellites" v-bind:key="satellite.id"
|
||||
@click.stop="onSatelliteClick(satellite.id)" >
|
||||
<svg v-if="satellite.disqualified" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" alt="disqualified image">
|
||||
<path d="M16.6625 13.9324C16.441 13.5232 16.2181 13.1133 15.996 12.702C15.5825 11.9391 15.1698 11.1762 14.7571 10.4133C14.2614 9.4992 13.7671 8.58373 13.2721 7.66969C12.7939 6.78728 12.3172 5.90625 11.8391 5.02459C11.4875 4.37631 11.1374 3.72733 10.7858 3.07843C10.6909 2.90265 10.596 2.72688 10.501 2.55249C10.4026 2.36968 10.2985 2.1939 10.1495 2.04484C9.62918 1.521 8.76574 1.42045 8.13997 1.81633C7.87137 1.98648 7.67732 2.22766 7.52826 2.50398C7.29975 2.92587 7.07122 3.34773 6.84271 3.77102C6.42646 4.54093 6.00951 5.31087 5.59326 6.08078C5.09192 6.99977 4.59482 7.92092 4.0956 8.84198C3.6231 9.71527 3.1499 10.5871 2.6767 11.4612C2.33076 12.101 1.98411 12.7394 1.63749 13.3792C1.54608 13.548 1.45468 13.7167 1.36328 13.8855C1.23531 14.1231 1.13477 14.3636 1.10312 14.6378C1.01383 15.4127 1.55663 16.1763 2.30687 16.3661C2.50516 16.4167 2.70062 16.4189 2.90102 16.4189H15.276H15.2957C15.7035 16.4104 16.0902 16.2571 16.3891 15.9794C16.6773 15.7122 16.8461 15.3466 16.8939 14.9599C16.9396 14.5957 16.8341 14.2511 16.6626 13.9326L16.6625 13.9324ZM8.29666 6.27882C8.29666 5.88507 8.6187 5.59327 8.99978 5.5757C9.37947 5.55812 9.70289 5.9118 9.70289 6.27882V11.2303C9.70289 11.624 9.38085 11.9158 8.99978 11.9334C8.62008 11.951 8.29666 11.5973 8.29666 11.2303V6.27882ZM8.99978 14.3282C8.60462 14.3282 8.28399 14.0083 8.28399 13.6124C8.28399 13.2173 8.6039 12.8967 8.99978 12.8967C9.39493 12.8967 9.71556 13.2166 9.71556 13.6124C9.71556 14.0076 9.39495 14.3282 8.99978 14.3282Z" fill="#F4D638"/>
|
||||
</svg>
|
||||
<p :class="{disqualified: satellite.disqualified}">{{satellite.id}}</p>
|
||||
</div>
|
||||
<div class="satellite-selection-choice-container__all-satellites">
|
||||
<div class="satellite-selection-overflow-container__satellite-choice" @click.stop="onSatelliteClick(null)">
|
||||
<p :class="{selected: !selectedSatellite}">All Satellites</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
|
||||
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
||||
import { SatelliteInfo } from '@/storagenode/dashboard';
|
||||
|
||||
@Component
|
||||
export default class SatelliteSelectionDropdown extends Vue {
|
||||
public async onSatelliteClick(id: string): Promise<void> {
|
||||
await this.$store.dispatch(NODE_ACTIONS.SELECT_SATELLITE, id);
|
||||
this.$store.dispatch(APPSTATE_ACTIONS.TOGGLE_SATELLITE_SELECTION);
|
||||
}
|
||||
|
||||
public get satellites(): SatelliteInfo[] {
|
||||
return this.$store.state.node.satellites;
|
||||
}
|
||||
|
||||
public get selectedSatellite(): string {
|
||||
return this.$store.state.node.selectedSatellite.id;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.satellite-selection-choice-container {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
padding: 7px 0 7px 0;
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||
background-color: #FFFFFF;
|
||||
z-index: 1120;
|
||||
}
|
||||
|
||||
.satellite-selection-overflow-container {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: auto;
|
||||
|
||||
&__satellite-choice {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: calc(100% - 28px);
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 0 0 0 12px;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #EBECF0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__all-satellites {
|
||||
padding: 0 0 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.disqualified {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 5px #fff;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #AFB7C1;
|
||||
border-radius: 6px;
|
||||
height: 5px;
|
||||
}
|
||||
</style>
|
27
web/storagenode/src/app/router/index.ts
Normal file
27
web/storagenode/src/app/router/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
|
||||
import { NavigationLink } from '@/app/types/navigation';
|
||||
import Dashboard from '@/app/views/Dashboard.vue';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export abstract class RouteConfig {
|
||||
public static Root = new NavigationLink('', 'Root');
|
||||
}
|
||||
|
||||
const router = new Router({
|
||||
mode: 'history',
|
||||
routes: [
|
||||
{
|
||||
path: RouteConfig.Root.path,
|
||||
name: RouteConfig.Root.name,
|
||||
component: Dashboard
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
export default router;
|
20
web/storagenode/src/app/store/index.ts
Normal file
20
web/storagenode/src/app/store/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { appStateModule } from './modules/appState';
|
||||
import { node } from './modules/node';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
// storage node store (vuex)
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
node,
|
||||
appStateModule,
|
||||
}
|
||||
});
|
||||
|
||||
export default store;
|
41
web/storagenode/src/app/store/modules/appState.ts
Normal file
41
web/storagenode/src/app/store/modules/appState.ts
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
export const APPSTATE_MUTATIONS = {
|
||||
TOGGLE_SATELLITE_SELECTION: 'TOGGLE_SATELLITE_SELECTION',
|
||||
CLOSE_ALL: 'CLOSE_ALL',
|
||||
};
|
||||
|
||||
export const APPSTATE_ACTIONS = {
|
||||
TOGGLE_SATELLITE_SELECTION: 'TOGGLE_SATELLITE_SELECTION',
|
||||
};
|
||||
|
||||
const {
|
||||
TOGGLE_SATELLITE_SELECTION,
|
||||
CLOSE_ALL,
|
||||
} = APPSTATE_MUTATIONS;
|
||||
|
||||
export const appStateModule = {
|
||||
state: {
|
||||
isSatelliteSelectionShown: false,
|
||||
},
|
||||
mutations: {
|
||||
[TOGGLE_SATELLITE_SELECTION](state: any): void {
|
||||
state.isSatelliteSelectionShown = !state.isSatelliteSelectionShown;
|
||||
},
|
||||
[CLOSE_ALL](state: any): void {
|
||||
state.isSatelliteSelectionShown = false;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[APPSTATE_ACTIONS.TOGGLE_SATELLITE_SELECTION]: function ({commit, state}: any): void {
|
||||
if (!state.isSatelliteSelectionShown) {
|
||||
commit(APPSTATE_MUTATIONS.TOGGLE_SATELLITE_SELECTION);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
commit(APPSTATE_MUTATIONS.CLOSE_ALL);
|
||||
},
|
||||
},
|
||||
};
|
161
web/storagenode/src/app/store/modules/node.ts
Normal file
161
web/storagenode/src/app/store/modules/node.ts
Normal file
@ -0,0 +1,161 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { SNOApi } from '@/storagenode/api/storagenode';
|
||||
import { Dashboard, SatelliteInfo } from '@/storagenode/dashboard';
|
||||
import { BandwidthUsed, Satellite, Stamp } from '@/storagenode/satellite';
|
||||
|
||||
export const NODE_MUTATIONS = {
|
||||
POPULATE_STORE: 'POPULATE_STORE',
|
||||
SELECT_SATELLITE: 'SELECT_SATELLITE',
|
||||
};
|
||||
|
||||
export const NODE_ACTIONS = {
|
||||
GET_NODE_INFO: 'GET_NODE_INFO',
|
||||
SELECT_SATELLITE: 'SELECT_SATELLITE',
|
||||
};
|
||||
|
||||
export const StatusOnline = 'Online';
|
||||
export const StatusOffline = 'Offline';
|
||||
|
||||
const {
|
||||
POPULATE_STORE,
|
||||
SELECT_SATELLITE,
|
||||
} = NODE_MUTATIONS;
|
||||
|
||||
const {
|
||||
GET_NODE_INFO,
|
||||
} = NODE_ACTIONS;
|
||||
|
||||
const statusThreshHoldMinutes = 10;
|
||||
const snoAPI = new SNOApi();
|
||||
|
||||
const allSatellites = {
|
||||
id: null,
|
||||
disqualified: null,
|
||||
};
|
||||
|
||||
export const node = {
|
||||
state: {
|
||||
info: {
|
||||
id: '',
|
||||
status: StatusOffline,
|
||||
version: '',
|
||||
wallet: '',
|
||||
isLastVersion: false
|
||||
},
|
||||
utilization: {
|
||||
bandwidth: {
|
||||
used: 0,
|
||||
remaining: 1,
|
||||
available: 1,
|
||||
},
|
||||
diskSpace: {
|
||||
used: 0,
|
||||
remaining: 1,
|
||||
available: 1,
|
||||
},
|
||||
},
|
||||
satellites: new Array<SatelliteInfo>(),
|
||||
disqualifiedSatellites: new Array<SatelliteInfo>(),
|
||||
selectedSatellite: allSatellites,
|
||||
bandwidthChartData: new Array<BandwidthUsed>(),
|
||||
storageChartData: new Array<Stamp>(),
|
||||
storageSummary: 0,
|
||||
bandwidthSummary: 0,
|
||||
checks: {
|
||||
uptime: 0,
|
||||
audit: 0,
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[POPULATE_STORE](state: any, nodeInfo: Dashboard): void {
|
||||
state.info.id = nodeInfo.nodeID;
|
||||
state.info.isLastVersion = nodeInfo.isUpToDate;
|
||||
state.info.version = nodeInfo.version;
|
||||
state.info.wallet = nodeInfo.wallet;
|
||||
state.utilization.diskSpace.used = nodeInfo.diskSpace.used;
|
||||
state.utilization.diskSpace.remaining = nodeInfo.diskSpace.available - nodeInfo.diskSpace.used;
|
||||
state.utilization.diskSpace.available = nodeInfo.diskSpace.available;
|
||||
state.utilization.bandwidth.used = nodeInfo.bandwidth.used;
|
||||
state.utilization.bandwidth.remaining = nodeInfo.bandwidth.available - nodeInfo.bandwidth.used;
|
||||
state.utilization.bandwidth.available = nodeInfo.bandwidth.available;
|
||||
state.disqualifiedSatellites = [];
|
||||
|
||||
state.satellites = nodeInfo.satellites ? nodeInfo.satellites : [];
|
||||
|
||||
state.info.status = StatusOffline;
|
||||
|
||||
if (getDateDiffMinutes(new Date(), new Date(nodeInfo.lastPinged)) < statusThreshHoldMinutes) {
|
||||
state.info.status = StatusOnline;
|
||||
}
|
||||
},
|
||||
[SELECT_SATELLITE](state: any, satelliteInfo: Satellite): void {
|
||||
if (satelliteInfo.id) {
|
||||
state.satellites.forEach(satellite => {
|
||||
if (satelliteInfo.id === satellite.id) {
|
||||
const audit = calculateSuccessRatio(
|
||||
satelliteInfo.audit.successCount,
|
||||
satelliteInfo.audit.totalCount
|
||||
);
|
||||
|
||||
const uptime = calculateSuccessRatio(
|
||||
satelliteInfo.uptime.successCount,
|
||||
satelliteInfo.uptime.totalCount
|
||||
);
|
||||
|
||||
state.selectedSatellite = satellite;
|
||||
state.checks.audit = audit;
|
||||
state.checks.uptime = uptime;
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
else {
|
||||
state.selectedSatellite = allSatellites;
|
||||
}
|
||||
|
||||
state.bandwidthChartData = satelliteInfo.bandwidthDaily;
|
||||
state.storageChartData = satelliteInfo.storageDaily;
|
||||
state.bandwidthSummary = satelliteInfo.bandwidthSummary;
|
||||
state.storageSummary = satelliteInfo.storageSummary;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[GET_NODE_INFO]: async function ({commit}: any): Promise<any> {
|
||||
const response = await snoAPI.dashboard();
|
||||
|
||||
commit(NODE_MUTATIONS.POPULATE_STORE, response);
|
||||
},
|
||||
[NODE_ACTIONS.SELECT_SATELLITE]: async function ({commit}, id: any): Promise<any> {
|
||||
const response = id ? await snoAPI.satellite(id) : await snoAPI.satellites();
|
||||
|
||||
commit(NODE_MUTATIONS.SELECT_SATELLITE, response);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* calculates percent of success attempts for reputation metric
|
||||
* @param successCount - holds amount of success attempts for reputation metric
|
||||
* @param totalCount - holds total amount of attempts for reputation metric
|
||||
*/
|
||||
function calculateSuccessRatio(successCount: number, totalCount: number) : number {
|
||||
if (totalCount === 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return successCount / totalCount * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns difference between two dates in minutes
|
||||
* @param d1 - holds first date
|
||||
* @param d2 - holds second date
|
||||
*/
|
||||
function getDateDiffMinutes(d1: Date, d2: Date): number {
|
||||
const diff = d1.getTime() - d2.getTime();
|
||||
|
||||
return Math.floor(diff / 1000 / 60);
|
||||
}
|
35
web/storagenode/src/app/types/chartData.ts
Normal file
35
web/storagenode/src/app/types/chartData.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2019 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
23
web/storagenode/src/app/types/navigation.ts
Normal file
23
web/storagenode/src/app/types/navigation.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* NavigationLink class holds info for NavigationLink entity.
|
||||
*/
|
||||
export class NavigationLink {
|
||||
private _path: string;
|
||||
private _name: string;
|
||||
|
||||
public constructor(path: string, name: string) {
|
||||
this._path = path;
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
public get path(): string {
|
||||
return this._path;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
}
|
7
web/storagenode/src/app/types/vue.d.ts
vendored
Normal file
7
web/storagenode/src/app/types/vue.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
109
web/storagenode/src/app/utils/chartUtils.ts
Normal file
109
web/storagenode/src/app/utils/chartUtils.ts
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { GB, KB, MB } from '@/app/utils/converter';
|
||||
import { BandwidthUsed, Stamp } from '@/storagenode/satellite';
|
||||
|
||||
/**
|
||||
* Used to display correct and convenient data on chart
|
||||
*/
|
||||
export class ChartUtils {
|
||||
/**
|
||||
* 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 = GB;
|
||||
switch (true) {
|
||||
case maxBytes < MB:
|
||||
divider = KB;
|
||||
break;
|
||||
case maxBytes < GB:
|
||||
divider = MB;
|
||||
break;
|
||||
}
|
||||
|
||||
return data.map(elem => elem / divider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to display correct number of days on chart's labels
|
||||
* @param date - holds specific day of the month
|
||||
* @returns daysDisplayed - array of days converted to a string by using the current or specified locale
|
||||
*/
|
||||
public static daysDisplayedOnChart(date: Date): string[] {
|
||||
const daysDisplayed = Array<string>(date.getDate());
|
||||
|
||||
for (let i = 0; i < daysDisplayed.length; i++) {
|
||||
const date = new Date();
|
||||
date.setDate(i + 1);
|
||||
|
||||
daysDisplayed[i] = date.toLocaleDateString('en-US', {day: 'numeric'});
|
||||
}
|
||||
|
||||
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: BandwidthUsed[]): BandwidthUsed[] {
|
||||
const bandwidthChartData: BandwidthUsed[] = new Array(new Date().getDate());
|
||||
const data: BandwidthUsed[] = fetchedData ? 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.getDate() === date) {
|
||||
bandwidthChartData[i] = data[j];
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
bandwidthChartData[i] = BandwidthUsed.emptyWithDate(date);
|
||||
}
|
||||
|
||||
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().getDate());
|
||||
const data: Stamp[] = fetchedData ? 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.getDate() === date) {
|
||||
storageChartData[i] = data[j];
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
storageChartData[i] = Stamp.emptyWithDate(date);
|
||||
}
|
||||
|
||||
return storageChartData;
|
||||
}
|
||||
}
|
28
web/storagenode/src/app/utils/converter.ts
Normal file
28
web/storagenode/src/app/utils/converter.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
export const KB = 1e3;
|
||||
export const MB = 1e6;
|
||||
export const GB = 1e9;
|
||||
|
||||
/**
|
||||
* Used to format amount from bytes to more compact unit
|
||||
* @param bytes - holds amount of bytes
|
||||
* @returns bytes - amount of formatted bytes with unit name
|
||||
*/
|
||||
export function formatBytes(bytes): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const decimals = 2;
|
||||
|
||||
const _bytes = Math.abs(bytes);
|
||||
|
||||
switch (true) {
|
||||
case _bytes < MB:
|
||||
return `${(bytes / KB).toFixed(decimals)}KB`;
|
||||
case _bytes < GB:
|
||||
return `${(bytes / MB).toFixed(decimals)}MB`;
|
||||
default:
|
||||
return `${(bytes / GB).toFixed(decimals)}GB`;
|
||||
}
|
||||
}
|
58
web/storagenode/src/app/views/Dashboard.vue
Normal file
58
web/storagenode/src/app/views/Dashboard.vue
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<SNOHeader />
|
||||
<div class="content">
|
||||
<SNOContentTitle />
|
||||
<SNOContentFilling />
|
||||
</div>
|
||||
<SNOFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import SNOContentFilling from '@/app/components/SNOContentFilling.vue';
|
||||
import SNOContentTitle from '@/app/components/SNOContentTitle.vue';
|
||||
import SNOFooter from '@/app/components/SNOFooter.vue';
|
||||
import SNOHeader from '@/app/components/SNOHeader.vue';
|
||||
import { NODE_ACTIONS } from '@/app/store/modules/node';
|
||||
|
||||
const {
|
||||
GET_NODE_INFO,
|
||||
SELECT_SATELLITE,
|
||||
} = NODE_ACTIONS;
|
||||
|
||||
@Component ({
|
||||
components: {
|
||||
SNOHeader,
|
||||
SNOContentTitle,
|
||||
SNOContentFilling,
|
||||
SNOFooter,
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
public async mounted() {
|
||||
await this.$store.dispatch(GET_NODE_INFO);
|
||||
await this.$store.dispatch(SELECT_SATELLITE, null);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
background-color: #F4F6F9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 822px;
|
||||
padding-top: 31px;
|
||||
}
|
||||
</style>
|
16
web/storagenode/src/main.ts
Normal file
16
web/storagenode/src/main.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.\
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import App from './app/App.vue';
|
||||
import router from './app/router';
|
||||
import store from './app/store';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: (h) => h(App),
|
||||
store,
|
||||
}).$mount('#app');
|
105
web/storagenode/src/storagenode/api/storagenode.ts
Normal file
105
web/storagenode/src/storagenode/api/storagenode.ts
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { BandwidthInfo, Dashboard, DiskSpaceInfo, SatelliteInfo, Version } from '@/storagenode/dashboard';
|
||||
import { BandwidthUsed, Egress, Ingress, Metric, Satellite, Satellites, Stamp } from '@/storagenode/satellite';
|
||||
|
||||
/**
|
||||
* Implementation for HTTP GET requests
|
||||
* @param url - holds url of request target
|
||||
* @throws Error - holds error message if request wasn't successful
|
||||
*/
|
||||
async function httpGet(url): Promise<Response> {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) {
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* used to get dashboard and satellite data from json
|
||||
*/
|
||||
export class SNOApi {
|
||||
/**
|
||||
* parses dashboard data from json
|
||||
* @returns dashboard - new dashboard instance filled with data from json
|
||||
*/
|
||||
public async dashboard(): Promise<Dashboard> {
|
||||
const json = (await (await httpGet('/api/dashboard')).json() as any).data;
|
||||
|
||||
const satellites: SatelliteInfo[] = json.satellites.map((satellite: any) => {
|
||||
const disqualified: Date | null = satellite.disqualified ? new Date(satellite.disqualified) : null;
|
||||
|
||||
return new SatelliteInfo(satellite.id, disqualified);
|
||||
});
|
||||
|
||||
const version: Version = new Version(json.version.major, json.version.minor, json.version.patch);
|
||||
|
||||
const diskSpace: DiskSpaceInfo = new DiskSpaceInfo(json.diskSpace.used, json.diskSpace.available);
|
||||
|
||||
const bandwidth: BandwidthInfo = new BandwidthInfo(json.bandwidth.used, json.bandwidth.available);
|
||||
|
||||
return new Dashboard(json.nodeID, json.wallet, satellites, diskSpace, bandwidth,
|
||||
new Date(json.lastPinged), new Date(json.lastQueried), version, json.upToDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* parses satellite data from json
|
||||
* @returns satellite - new satellite instance filled with data from json
|
||||
*/
|
||||
public async satellite(id: string): Promise<Satellite> {
|
||||
const url = '/api/satellite/' + id;
|
||||
|
||||
const json = (await (await httpGet(url)).json() as any).data;
|
||||
|
||||
const storageDailyJson = json.storageDaily ? json.storageDaily : [];
|
||||
const bandwidthDailyJson = json.bandwidthDaily ? json.bandwidthDaily : [];
|
||||
|
||||
const storageDaily: Stamp[] = storageDailyJson.map((stamp: any) => {
|
||||
return new Stamp(stamp.atRestTotal, new Date(stamp.intervalStart));
|
||||
});
|
||||
|
||||
const bandwidthDaily: BandwidthUsed[] = bandwidthDailyJson.map((bandwidth: any) => {
|
||||
const egress = new Egress(bandwidth.egress.audit, bandwidth.egress.repair, bandwidth.egress.usage);
|
||||
const ingress = new Ingress(bandwidth.ingress.repair, bandwidth.ingress.usage);
|
||||
|
||||
return new BandwidthUsed(egress, ingress, new Date(bandwidth.intervalStart));
|
||||
});
|
||||
|
||||
const audit: Metric = new Metric(json.audit.totalCount, json.audit.successCount, json.audit.alpha,
|
||||
json.audit.beta, json.audit.score);
|
||||
|
||||
const uptime: Metric = new Metric(json.uptime.totalCount, json.uptime.successCount, json.uptime.alpha,
|
||||
json.uptime.beta, json.uptime.score);
|
||||
|
||||
return new Satellite(json.id, storageDaily, bandwidthDaily, json.storageSummary,
|
||||
json.bandwidthSummary, audit, uptime);
|
||||
}
|
||||
|
||||
/**
|
||||
* parses data for all satellites from json
|
||||
* @returns satellites - new satellites instance filled with data from json
|
||||
*/
|
||||
public async satellites(): Promise<Satellites> {
|
||||
const json = (await (await httpGet('/api/satellites')).json() as any).data;
|
||||
|
||||
const storageDailyJson = json.storageDaily ? json.storageDaily : [];
|
||||
const bandwidthDailyJson = json.bandwidthDaily ? json.bandwidthDaily : [];
|
||||
|
||||
const storageDaily: Stamp[] = storageDailyJson.map((stamp: any) => {
|
||||
return new Stamp(stamp.atRestTotal, new Date(stamp.intervalStart));
|
||||
});
|
||||
|
||||
const bandwidthDaily: BandwidthUsed[] = bandwidthDailyJson.map((bandwidth: any) => {
|
||||
const egress = new Egress(bandwidth.egress.audit, bandwidth.egress.repair, bandwidth.egress.usage);
|
||||
const ingress = new Ingress(bandwidth.ingress.repair, bandwidth.ingress.usage);
|
||||
|
||||
return new BandwidthUsed(egress, ingress, new Date(bandwidth.intervalStart));
|
||||
});
|
||||
|
||||
return new Satellites(storageDaily, bandwidthDaily, json.storageSummary, json.bandwidthSummary);
|
||||
}
|
||||
}
|
72
web/storagenode/src/storagenode/dashboard.ts
Normal file
72
web/storagenode/src/storagenode/dashboard.ts
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* Dashboard encapsulates dashboard stale data
|
||||
*/
|
||||
export class Dashboard {
|
||||
public constructor(
|
||||
public nodeID: string,
|
||||
public wallet: string,
|
||||
public satellites: SatelliteInfo[],
|
||||
public diskSpace: DiskSpaceInfo,
|
||||
public bandwidth: BandwidthInfo,
|
||||
public lastPinged: Date,
|
||||
public lastQueried: Date,
|
||||
public version: Version,
|
||||
public isUpToDate: boolean) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Version represents a semantic version
|
||||
*/
|
||||
export class Version {
|
||||
public constructor(
|
||||
public major: number,
|
||||
public minor: number,
|
||||
public patch: number) {}
|
||||
|
||||
/**
|
||||
* Converts version numbers to string type
|
||||
* @returns version - string of version value
|
||||
*/
|
||||
public toString(): string {
|
||||
return `v${this.major}.${this.minor}.${this.patch}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SatelliteInfo encapsulates satellite ID and disqualification
|
||||
*/
|
||||
export class SatelliteInfo {
|
||||
public constructor(
|
||||
public id: string,
|
||||
public disqualified: Date | null,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* DiskSpaceInfo stores all info about storage node disk space usage
|
||||
*/
|
||||
export class DiskSpaceInfo {
|
||||
public remaining: number;
|
||||
|
||||
public constructor(
|
||||
public used: number,
|
||||
public available: number) {
|
||||
this.remaining = available - used;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BandwidthInfo stores all info about storage node bandwidth usage
|
||||
*/
|
||||
export class BandwidthInfo {
|
||||
public remaining: number;
|
||||
|
||||
public constructor(
|
||||
public used: number,
|
||||
public available: number) {
|
||||
this.remaining = available - used;
|
||||
}
|
||||
}
|
114
web/storagenode/src/storagenode/satellite.ts
Normal file
114
web/storagenode/src/storagenode/satellite.ts
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* Satellite encapsulates satellite related data
|
||||
*/
|
||||
export class Satellite {
|
||||
public constructor(
|
||||
public id: string,
|
||||
public storageDaily: Stamp[],
|
||||
public bandwidthDaily: BandwidthUsed[],
|
||||
public storageSummary: number,
|
||||
public bandwidthSummary: number,
|
||||
public audit: Metric,
|
||||
public uptime: Metric) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, intervalStart: 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.setDate(date);
|
||||
|
||||
return new Stamp(0, now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric encapsulates storagenode reputation metrics
|
||||
*/
|
||||
export class Metric {
|
||||
public constructor(
|
||||
public totalCount: number,
|
||||
public successCount: number,
|
||||
public alpha: number,
|
||||
public beta: number,
|
||||
public score: number) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Egress stores info about storage node egress usage
|
||||
*/
|
||||
export class Egress {
|
||||
public constructor(
|
||||
public audit: number,
|
||||
public repair: number,
|
||||
public usage: number) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingress stores info about storage node ingress usage
|
||||
*/
|
||||
export class Ingress {
|
||||
public constructor(
|
||||
public repair: number,
|
||||
public usage: number) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* BandwidthUsed stores bandwidth usage information over the period of time
|
||||
*/
|
||||
export class BandwidthUsed {
|
||||
public constructor(
|
||||
public egress: Egress,
|
||||
public ingress: Ingress,
|
||||
public intervalStart: Date) {}
|
||||
|
||||
/**
|
||||
* Used to summarize all bandwidth usage data
|
||||
* @returns summary - sum of all bandwidth usage data
|
||||
*/
|
||||
public summary(): number {
|
||||
return this.egress.audit + this.egress.repair + this.egress.usage +
|
||||
this.ingress.repair + this.ingress.usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): BandwidthUsed {
|
||||
const now = new Date();
|
||||
now.setDate(date);
|
||||
|
||||
return new BandwidthUsed(new Egress(0, 0, 0), new Ingress(0, 0), now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Satellites encapsulate related data of all satellites
|
||||
*/
|
||||
export class Satellites {
|
||||
public constructor(
|
||||
public storageDaily: Stamp[],
|
||||
public bandwidthDaily: BandwidthUsed[],
|
||||
public storageSummary: number,
|
||||
public bandwidthSummary: number) {}
|
||||
}
|
BIN
web/storagenode/static/fonts/font_bold.ttf
Normal file
BIN
web/storagenode/static/fonts/font_bold.ttf
Normal file
Binary file not shown.
BIN
web/storagenode/static/fonts/font_medium.ttf
Normal file
BIN
web/storagenode/static/fonts/font_medium.ttf
Normal file
Binary file not shown.
BIN
web/storagenode/static/fonts/font_regular.ttf
Normal file
BIN
web/storagenode/static/fonts/font_regular.ttf
Normal file
Binary file not shown.
BIN
web/storagenode/static/images/Message.png
Normal file
BIN
web/storagenode/static/images/Message.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
39
web/storagenode/tsconfig.json
Normal file
39
web/storagenode/tsconfig.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"strictPropertyInitialization": false,
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
87
web/storagenode/tslint.json
Normal file
87
web/storagenode/tslint.json
Normal file
@ -0,0 +1,87 @@
|
||||
{
|
||||
"defaultSeverity": "warning",
|
||||
"rulesDirectory": [
|
||||
"tslint-consistent-codestyle"
|
||||
],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"function-constructor": true,
|
||||
"align": [true, "parameters", "statements"],
|
||||
"array-type": [true, "array-simple"],
|
||||
"arrow-return-shorthand": true,
|
||||
"check-format": true,
|
||||
"class-name": true,
|
||||
"comment-format": [true, "check-space"],
|
||||
"comment-type": [true, "doc", "singleline"],
|
||||
"curly": [true, "ignore-same-line"],
|
||||
"early-exit": true,
|
||||
"eofline": true,
|
||||
"indent": [true, "spaces", 4],
|
||||
"interface-name": false,
|
||||
"import-spacing": true,
|
||||
"no-async-without-await": true,
|
||||
"no-boolean-literal-compare": true,
|
||||
"no-conditional-assignment": true,
|
||||
"no-consecutive-blank-lines": [true, 1],
|
||||
"no-console": [true, "log"],
|
||||
"no-default-export": false,
|
||||
"no-duplicate-imports": true,
|
||||
"no-duplicate-super": true,
|
||||
"no-duplicate-switch-case": true,
|
||||
"no-empty": true,
|
||||
"no-eval": true,
|
||||
"no-invalid-template-strings": true,
|
||||
"no-invalid-this": true,
|
||||
"no-static-this": true,
|
||||
"no-var-keyword": true,
|
||||
"newline-before-return": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-variable-per-declaration": [true, "ignore-for-loop"],
|
||||
"ordered-imports": [
|
||||
true, {
|
||||
"import-sources-order": "case-insensitive",
|
||||
"named-imports-order": "case-insensitive",
|
||||
"grouped-imports": true,
|
||||
"groups": [{
|
||||
"name": "external",
|
||||
"match": "^[A-Za-z]",
|
||||
"order": 1
|
||||
}, {
|
||||
"name": "internal components",
|
||||
"match": "^@/components",
|
||||
"order": 2
|
||||
}, {
|
||||
"name": "internal else",
|
||||
"match": "^@",
|
||||
"order": 3
|
||||
}]
|
||||
}],
|
||||
"prefer-const": true,
|
||||
"prefer-switch": [true, {"min-cases": 2}],
|
||||
"prefer-while": true,
|
||||
"quotemark": [true, "single", "avoid-escape"],
|
||||
"semicolon": [true, "always"],
|
||||
"static-this": true,
|
||||
"triple-equals": true,
|
||||
"typedef": [
|
||||
true,
|
||||
"property-declaration"
|
||||
],
|
||||
"type-literal-delimiter": true,
|
||||
"unnecessary-else": true,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-module",
|
||||
"check-separator",
|
||||
"check-type-operator",
|
||||
"check-preblock"
|
||||
]
|
||||
}
|
||||
}
|
24
web/storagenode/vue.config.js
Normal file
24
web/storagenode/vue.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
publicPath: "/static/dist",
|
||||
productionSourceMap: false,
|
||||
parallel: true,
|
||||
chainWebpack: config => {
|
||||
config.output.chunkFilename(`js/vendors.js`);
|
||||
config.output.filename(`js/app.js`);
|
||||
|
||||
config.resolve.alias
|
||||
.set('@', path.resolve('src'));
|
||||
|
||||
config
|
||||
.plugin('html')
|
||||
.tap(args => {
|
||||
args[0].template = './index.html';
|
||||
return args
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user