web/satellite: implemented charts UI for new project dashboard
Added bandwidth/storage charts (with test data) to new project dashboard. Added functional buttons to new project dashboard. Fixed this issue https://github.com/storj/storj/issues/4262. Change-Id: Ie87370b8f7b6015bc84022a6086ef1db40e16535
This commit is contained in:
parent
b78f65e83b
commit
be10ce84f8
108
web/satellite/package-lock.json
generated
108
web/satellite/package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"aws-sdk": "2.853.0",
|
||||
"bip39": "3.0.3",
|
||||
"browser": "git+https://github.com/storj/browser#4e5770dfa9883176bbab793138911d92305243ed",
|
||||
"chart.js": "2.9.4",
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"load-script": "1.0.0",
|
||||
@ -25,6 +26,7 @@
|
||||
"qrcode": "1.4.4",
|
||||
"stripe": "8.96.0",
|
||||
"vue": "2.6.12",
|
||||
"vue-chartjs": "3.5.1",
|
||||
"vue-class-component": "7.2.5",
|
||||
"vue-clipboard2": "0.3.1",
|
||||
"vue-property-decorator": "9.0.0",
|
||||
@ -3834,6 +3836,14 @@
|
||||
"magic-string": "^0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.34.tgz",
|
||||
"integrity": "sha512-CtZVk+kh1IN67dv+fB0CWmCLCRrDJgqOj15qPic2B1VCMovNO6B7Vhf/TgPpNscjhAL1j+qUntDMWb9A4ZmPTg==",
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@ -9507,6 +9517,32 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||
"dependencies": {
|
||||
"chartjs-color": "^2.1.0",
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"chartjs-color-string": "^0.6.0",
|
||||
"color-convert": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/check-types": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
|
||||
@ -21288,6 +21324,14 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@ -28413,6 +28457,21 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||
},
|
||||
"node_modules/vue-chartjs": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
|
||||
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
|
||||
"dependencies": {
|
||||
"@types/chart.js": "^2.7.55"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"chart.js": ">= 2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-class-component": {
|
||||
"version": "7.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.5.tgz",
|
||||
@ -33332,6 +33391,14 @@
|
||||
"magic-string": "^0.25.0"
|
||||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.34.tgz",
|
||||
"integrity": "sha512-CtZVk+kh1IN67dv+fB0CWmCLCRrDJgqOj15qPic2B1VCMovNO6B7Vhf/TgPpNscjhAL1j+qUntDMWb9A4ZmPTg==",
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@ -37914,6 +37981,32 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||
"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": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
|
||||
@ -39995,7 +40088,7 @@
|
||||
"eslint-plugin-storj": {
|
||||
"version": "git+https://git@github.com/storj/eslint-storj.git#5f952ffab7141e752cc095e5f024c39bab89679f",
|
||||
"dev": true,
|
||||
"from": "eslint-plugin-storj@https://github.com/storj/eslint-storj"
|
||||
"from": "eslint-plugin-storj@github:storj/eslint-storj"
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "7.16.0",
|
||||
@ -47219,6 +47312,11 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@ -52925,6 +53023,14 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||
},
|
||||
"vue-chartjs": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
|
||||
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
|
||||
"requires": {
|
||||
"@types/chart.js": "^2.7.55"
|
||||
}
|
||||
},
|
||||
"vue-class-component": {
|
||||
"version": "7.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.5.tgz",
|
||||
|
@ -22,6 +22,7 @@
|
||||
"aws-sdk": "2.853.0",
|
||||
"bip39": "3.0.3",
|
||||
"browser": "git+https://github.com/storj/browser#4e5770dfa9883176bbab793138911d92305243ed",
|
||||
"chart.js": "2.9.4",
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"load-script": "1.0.0",
|
||||
@ -30,6 +31,7 @@
|
||||
"qrcode": "1.4.4",
|
||||
"stripe": "8.96.0",
|
||||
"vue": "2.6.12",
|
||||
"vue-chartjs": "3.5.1",
|
||||
"vue-class-component": "7.2.5",
|
||||
"vue-clipboard2": "0.3.1",
|
||||
"vue-property-decorator": "9.0.0",
|
||||
|
28
web/satellite/src/components/common/BaseChart.vue
Normal file
28
web/satellite/src/components/common/BaseChart.vue
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
|
||||
// @vue/component
|
||||
@Component
|
||||
export default class BaseChart extends Vue {
|
||||
@Prop({default: 0})
|
||||
public readonly width: number;
|
||||
@Prop({default: 0})
|
||||
public readonly height: number;
|
||||
|
||||
/**
|
||||
* Used for chart re rendering.
|
||||
*/
|
||||
public chartKey = 0;
|
||||
|
||||
/**
|
||||
* Triggers chart re rendering if chart width is being changed.
|
||||
*/
|
||||
@Watch('width')
|
||||
public rebuildChart(): void {
|
||||
this.chartKey += 1;
|
||||
}
|
||||
}
|
||||
</script>
|
165
web/satellite/src/components/common/VChart.vue
Normal file
165
web/satellite/src/components/common/VChart.vue
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<script lang="ts">
|
||||
import { Line } from 'vue-chartjs';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { ChartData, RenderChart } from '@/types/chart';
|
||||
|
||||
/**
|
||||
* Used to filter days displayed on x-axis.
|
||||
*/
|
||||
class DayShowingConditions {
|
||||
public constructor(
|
||||
public day: string,
|
||||
public daysArray: string[],
|
||||
) {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
extends: Line,
|
||||
})
|
||||
export default class VChart extends Vue {
|
||||
@Prop({ default: () => { console.error('Tooltip constructor is undefined'); } })
|
||||
private tooltipConstructor: (tooltipModel) => void;
|
||||
@Prop({ default: {} })
|
||||
private readonly chartData: ChartData;
|
||||
|
||||
/**
|
||||
* Mounted hook after initial render.
|
||||
* Adds chart plugin to draw dashed line under data point.
|
||||
* Renders chart.
|
||||
*/
|
||||
public mounted(): void {
|
||||
(this as unknown as RenderChart).addPlugin({
|
||||
afterDatasetsDraw: (chart): void => {
|
||||
if (chart.tooltip._active && chart.tooltip._active.length) {
|
||||
const activePoint = chart.tooltip._active[0];
|
||||
const ctx = chart.ctx;
|
||||
const y_axis = chart.scales['y-axis-0'];
|
||||
const tooltipPosition = activePoint.tooltipPosition();
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.setLineDash([8, 5]);
|
||||
ctx.moveTo(tooltipPosition.x, tooltipPosition.y + 12);
|
||||
ctx.lineTo(tooltipPosition.x, y_axis.bottom);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = '#C8D3DE';
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
(this as unknown as RenderChart).renderChart(this.chartData, this.chartOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns chart options.
|
||||
*/
|
||||
public get chartOptions(): Record<string, unknown> {
|
||||
const filterCallback = this.filterDaysDisplayed;
|
||||
|
||||
return {
|
||||
responsive: false,
|
||||
maintainAspectRatios: false,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 40,
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: this.chartData.datasets.length === 1 ? 10 : 0,
|
||||
hoverRadius: 10,
|
||||
hitRadius: 8,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: false,
|
||||
}],
|
||||
xAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
fontFamily: 'font_regular',
|
||||
autoSkip: false,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
callback: filterCallback,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
},
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
|
||||
custom: (tooltipModel) => {
|
||||
this.tooltipConstructor(tooltipModel);
|
||||
},
|
||||
|
||||
labels: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as callback to filter days displayed on chart.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if days are shown on even days amount.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if days are shown on not even days amount.
|
||||
*/
|
||||
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>
|
@ -1,207 +0,0 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="project-dashboard">
|
||||
<h1 class="project-dashboard__title">Dashboard</h1>
|
||||
<VLoader v-if="isDataFetching" class="project-dashboard__loader" width="100px" height="100px" />
|
||||
<p v-else class="project-dashboard__subtitle">
|
||||
Your
|
||||
<span class="project-dashboard__subtitle__value">{{ limits.objectCount }} objects</span>
|
||||
are stored in
|
||||
<span class="project-dashboard__subtitle__value">{{ limits.segmentCount }} segments</span>
|
||||
around the world
|
||||
</p>
|
||||
<div class="project-dashboard__info">
|
||||
<InfoContainer
|
||||
title="Billing"
|
||||
:subtitle="status"
|
||||
:value="estimatedCharges | centsToDollars"
|
||||
:is-data-fetching="isDataFetching"
|
||||
>
|
||||
<template #side-value>
|
||||
<p class="project-dashboard__info__label">Will be charged during next billing period</p>
|
||||
</template>
|
||||
</InfoContainer>
|
||||
<InfoContainer
|
||||
class="project-dashboard__info__middle"
|
||||
title="Objects"
|
||||
:subtitle="`Updated ${now}`"
|
||||
:value="limits.objectCount"
|
||||
:is-data-fetching="isDataFetching"
|
||||
>
|
||||
<template #side-value>
|
||||
<p class="project-dashboard__info__label">Total of {{ usedFormatted }}</p>
|
||||
</template>
|
||||
</InfoContainer>
|
||||
<InfoContainer
|
||||
title="Segments"
|
||||
:subtitle="`Updated ${now}`"
|
||||
:value="limits.segmentCount"
|
||||
:is-data-fetching="isDataFetching"
|
||||
>
|
||||
<template #side-value>
|
||||
<a
|
||||
class="project-dashboard__info__link"
|
||||
href="https://docs.storj.io/dcs/billing-payment-and-accounts-1/pricing/billing-and-payment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more ->
|
||||
</a>
|
||||
</template>
|
||||
</InfoContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { PROJECTS_ACTIONS } from "@/store/modules/projects";
|
||||
import { PAYMENTS_ACTIONS } from "@/store/modules/payments";
|
||||
import { RouteConfig } from "@/router";
|
||||
import { ProjectLimits } from "@/types/projects";
|
||||
import { Dimensions, Size } from "@/utils/bytesSize";
|
||||
|
||||
import VLoader from "@/components/common/VLoader.vue";
|
||||
import InfoContainer from "@/components/project/InfoContainer.vue";
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
components: {
|
||||
VLoader,
|
||||
InfoContainer,
|
||||
}
|
||||
})
|
||||
export default class NewProjectDashboard extends Vue {
|
||||
public now = new Date().toLocaleDateString('en-US');
|
||||
public isDataFetching = true;
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Fetches project limits.
|
||||
*/
|
||||
public async mounted(): Promise<void> {
|
||||
if (!this.$store.getters.selectedProject.id) {
|
||||
await this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
|
||||
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
|
||||
|
||||
this.isDataFetching = false;
|
||||
} catch (error) {
|
||||
await this.$notify.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current limits from store.
|
||||
*/
|
||||
public get limits(): ProjectLimits {
|
||||
return this.$store.state.projectsModule.currentLimits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns status string based on account status.
|
||||
*/
|
||||
public get status(): string {
|
||||
return this.$store.getters.user.paidTier ? 'Pro Account' : 'Free Account';
|
||||
}
|
||||
|
||||
/**
|
||||
* estimatedCharges returns estimated charges summary for selected project.
|
||||
*/
|
||||
public get estimatedCharges(): number {
|
||||
return this.$store.state.paymentsModule.priceSummaryForSelectedProject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns formatted used amount.
|
||||
*/
|
||||
public get usedFormatted(): string {
|
||||
return this.formattedValue(new Size(this.limits.storageUsed, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats value to needed form and returns it.
|
||||
*/
|
||||
private formattedValue(value: Size): string {
|
||||
switch (value.label) {
|
||||
case Dimensions.Bytes:
|
||||
case Dimensions.KB:
|
||||
return '0';
|
||||
default:
|
||||
return `${value.formattedBytes.replace(/\\.0+$/, '')}${value.label}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.project-dashboard {
|
||||
padding: 56px 40px;
|
||||
background-image: url('../../../static/images/project/background.png');
|
||||
background-position: top right;
|
||||
background-size: 70%;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&__loader {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #000;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
letter-spacing: -0.02em;
|
||||
color: #000;
|
||||
max-width: 365px;
|
||||
|
||||
&__value {
|
||||
text-decoration: underline;
|
||||
text-underline-position: under;
|
||||
text-decoration-color: #00e366;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 100px;
|
||||
|
||||
&__middle {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&__label,
|
||||
&__link {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&__link {
|
||||
text-decoration: underline !important;
|
||||
text-underline-position: under;
|
||||
|
||||
&:visited {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -5,14 +5,12 @@
|
||||
<div class="no-buckets-area">
|
||||
<img class="no-buckets-area__image" src="@/../static/images/buckets/bucket.png" alt="bucket image">
|
||||
<h2 class="no-buckets-area__message">Create your first bucket to get started.</h2>
|
||||
<a
|
||||
class="no-buckets-area__first-button"
|
||||
href="https://docs.storj.io/api-reference/uplink-cli"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
<VButton
|
||||
label="Get Started"
|
||||
width="156px"
|
||||
height="47px"
|
||||
:on-press="navigateToWelcomeScreen"
|
||||
/>
|
||||
<a
|
||||
class="no-buckets-area__second-button"
|
||||
href="https://docs.storj.io/"
|
||||
@ -27,9 +25,24 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { RouteConfig } from "@/router";
|
||||
|
||||
import VButton from "@/components/common/VButton.vue";
|
||||
|
||||
// @vue/component
|
||||
@Component
|
||||
export default class NoBucketArea extends Vue {}
|
||||
@Component({
|
||||
components: {
|
||||
VButton,
|
||||
}
|
||||
})
|
||||
export default class NoBucketArea extends Vue {
|
||||
/**
|
||||
* Navigates user to welcome screen.
|
||||
*/
|
||||
public navigateToWelcomeScreen(): void {
|
||||
this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -60,24 +73,6 @@ export default class NoBucketArea extends Vue {}
|
||||
margin: 15px 0 30px 0;
|
||||
}
|
||||
|
||||
&__first-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 158px;
|
||||
height: 49px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
border-radius: 6px;
|
||||
background-color: #2683ff;
|
||||
color: #fff;
|
||||
margin-bottom: 7px;
|
||||
|
||||
&:hover {
|
||||
background-color: #0059d0;
|
||||
}
|
||||
}
|
||||
|
||||
&__second-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -90,6 +85,7 @@ export default class NoBucketArea extends Vue {}
|
||||
background-color: #fff;
|
||||
color: #2683ff;
|
||||
border: 1px solid #2683ff;
|
||||
margin-top: 7px;
|
||||
|
||||
&:hover {
|
||||
background-color: #2683ff;
|
||||
|
@ -0,0 +1,135 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<VChart
|
||||
:id="`${name}-chart`"
|
||||
:key="chartKey"
|
||||
:chart-data="chartData"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:tooltip-constructor="tooltip"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
|
||||
import BaseChart from '@/components/common/BaseChart.vue';
|
||||
import VChart from '@/components/common/VChart.vue';
|
||||
|
||||
import { ChartData, Tooltip, TooltipParams, TooltipModel } from '@/types/chart';
|
||||
import { ChartUtils } from "@/utils/chart";
|
||||
import { DataStamp } from "@/types/projects";
|
||||
import { Size } from "@/utils/bytesSize";
|
||||
|
||||
/**
|
||||
* Stores data for chart's tooltip
|
||||
*/
|
||||
class ChartTooltip {
|
||||
public date: string;
|
||||
public value: string;
|
||||
|
||||
public constructor(storage: DataStamp) {
|
||||
const size = new Size(storage.value, 1)
|
||||
|
||||
this.date = storage.intervalStart.toLocaleDateString('en-US', { day: '2-digit', month: 'short' });
|
||||
this.value = `${size.formattedBytes} ${size.label}`;
|
||||
}
|
||||
}
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
components: { VChart }
|
||||
})
|
||||
export default class DashboardChart extends BaseChart {
|
||||
@Prop({default: []})
|
||||
public readonly data: DataStamp[];
|
||||
@Prop({default: 'chart'})
|
||||
public readonly name: string;
|
||||
@Prop({default: ''})
|
||||
public readonly backgroundColor: string;
|
||||
@Prop({default: ''})
|
||||
public readonly borderColor: string;
|
||||
@Prop({default: ''})
|
||||
public readonly pointBorderColor: string;
|
||||
|
||||
/**
|
||||
* Returns formatted data to render chart.
|
||||
*/
|
||||
public get chartData(): ChartData {
|
||||
const data: number[] = this.data.map(el => parseFloat(new Size(el.value).formattedBytes))
|
||||
const xAxisDateLabels: string[] = ChartUtils.daysDisplayedOnChart(this.data[0].intervalStart, this.data[this.data.length - 1].intervalStart);
|
||||
|
||||
return new ChartData(
|
||||
xAxisDateLabels,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.pointBorderColor,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as constructor of custom tooltip.
|
||||
*/
|
||||
public tooltip(tooltipModel: TooltipModel): void {
|
||||
const tooltipParams = new TooltipParams(tooltipModel, `${this.name}-chart`, `${this.name}-tooltip`,
|
||||
this.tooltipMarkUp(tooltipModel), 76, 81);
|
||||
|
||||
Tooltip.custom(tooltipParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tooltip's html mark up.
|
||||
*/
|
||||
private tooltipMarkUp(tooltipModel: TooltipModel): string {
|
||||
if (!tooltipModel.dataPoints) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dataIndex = tooltipModel.dataPoints[0].index;
|
||||
const dataPoint = new ChartTooltip(this.data[dataIndex]);
|
||||
|
||||
return `<div class='tooltip' style="background: ${this.pointBorderColor}">
|
||||
<p class='tooltip__value'>${dataPoint.date}<b class='tooltip__value__bold'> / ${dataPoint.value}</b></p>
|
||||
<div class='tooltip__arrow' style="background: ${this.pointBorderColor}" />
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tooltip {
|
||||
margin: 8px;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 14px rgba(9, 87, 203, 0.26);
|
||||
border-radius: 100px;
|
||||
padding-top: 8px;
|
||||
width: 145px;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__value {
|
||||
font-size: 14px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
|
||||
&__bold {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 8px 0 0 0;
|
||||
transform: scale(1, 0.85) translate(0, 20%) rotate(45deg);
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,447 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div ref="dashboard" class="project-dashboard">
|
||||
<h1 class="project-dashboard__title">Dashboard</h1>
|
||||
<VLoader v-if="isDataFetching" class="project-dashboard__loader" width="100px" height="100px" />
|
||||
<p v-if="!isDataFetching && limits.objectCount" class="project-dashboard__subtitle">
|
||||
Your
|
||||
<span class="project-dashboard__subtitle__value">{{ limits.objectCount }} objects</span>
|
||||
are stored in
|
||||
<span class="project-dashboard__subtitle__value">{{ limits.segmentCount }} segments</span>
|
||||
around the world
|
||||
</p>
|
||||
<template v-if="!isDataFetching && !limits.objectCount">
|
||||
<p class="project-dashboard__subtitle">
|
||||
Welcome to Storj :) <br> You’re ready to experience the future of cloud storage
|
||||
</p>
|
||||
<VButton
|
||||
class="project-dashboard__upload-button"
|
||||
label="Upload"
|
||||
width="100px"
|
||||
height="40px"
|
||||
:on-press="onUploadClick"
|
||||
/>
|
||||
</template>
|
||||
<div class="project-dashboard__stats-header">
|
||||
<h2 class="project-dashboard__stats-header__title">Project Stats</h2>
|
||||
<div class="project-dashboard__stats-header__buttons">
|
||||
<VButton
|
||||
v-if="!isProAccount"
|
||||
label="Upgrade Plan"
|
||||
width="114px"
|
||||
height="40px"
|
||||
:on-press="onUpgradeClick"
|
||||
is-white="true"
|
||||
/>
|
||||
<VButton
|
||||
v-else
|
||||
label="New Project"
|
||||
width="114px"
|
||||
height="40px"
|
||||
:on-press="onCreateProjectClick"
|
||||
:is-white="!limits.objectCount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-dashboard__charts">
|
||||
<div class="project-dashboard__charts__container">
|
||||
<h3 class="project-dashboard__charts__container__title">Storage</h3>
|
||||
<VLoader v-if="isDataFetching" class="project-dashboard__charts__container__loader" height="40px" width="40px" />
|
||||
<template v-else>
|
||||
<p class="project-dashboard__charts__container__info">
|
||||
Using {{ usedLimitFormatted(limits.storageUsed) }} of {{ usedLimitFormatted(limits.storageLimit) }}
|
||||
</p>
|
||||
<DashboardChart
|
||||
name="storage"
|
||||
:width="chartWidth"
|
||||
:height="170"
|
||||
:data="storageUsage"
|
||||
background-color="#E6EDF7"
|
||||
border-color="#D7E8FF"
|
||||
point-border-color="#003DC1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="project-dashboard__charts__container">
|
||||
<h3 class="project-dashboard__charts__container__title">Bandwidth</h3>
|
||||
<VLoader v-if="isDataFetching" class="project-dashboard__charts__container__loader" height="40px" width="40px" />
|
||||
<template v-else>
|
||||
<p class="project-dashboard__charts__container__info">
|
||||
Using {{ usedLimitFormatted(limits.bandwidthUsed) }} of {{ usedLimitFormatted(limits.bandwidthLimit) }}
|
||||
</p>
|
||||
<DashboardChart
|
||||
name="bandwidth"
|
||||
:width="chartWidth"
|
||||
:height="170"
|
||||
:data="bandwidthUsage"
|
||||
background-color="#FFE0E7"
|
||||
border-color="#FFC0CF"
|
||||
point-border-color="#FF458B"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-dashboard__info">
|
||||
<InfoContainer
|
||||
title="Billing"
|
||||
:subtitle="status"
|
||||
:value="estimatedCharges | centsToDollars"
|
||||
:is-data-fetching="isDataFetching"
|
||||
>
|
||||
<template #side-value>
|
||||
<p class="project-dashboard__info__label">Will be charged during next billing period</p>
|
||||
</template>
|
||||
</InfoContainer>
|
||||
<InfoContainer
|
||||
class="project-dashboard__info__middle"
|
||||
title="Objects"
|
||||
:subtitle="`Updated ${now}`"
|
||||
:value="limits.objectCount"
|
||||
:is-data-fetching="isDataFetching"
|
||||
>
|
||||
<template #side-value>
|
||||
<p class="project-dashboard__info__label">Total of {{ usedLimitFormatted(limits.storageUsed) }}</p>
|
||||
</template>
|
||||
</InfoContainer>
|
||||
<InfoContainer
|
||||
title="Segments"
|
||||
:subtitle="`Updated ${now}`"
|
||||
:value="limits.segmentCount"
|
||||
:is-data-fetching="isDataFetching"
|
||||
>
|
||||
<template #side-value>
|
||||
<a
|
||||
class="project-dashboard__info__link"
|
||||
href="https://docs.storj.io/dcs/billing-payment-and-accounts-1/pricing/billing-and-payment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more ->
|
||||
</a>
|
||||
</template>
|
||||
</InfoContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
import { PROJECTS_ACTIONS } from "@/store/modules/projects";
|
||||
import { PAYMENTS_ACTIONS, PAYMENTS_MUTATIONS } from "@/store/modules/payments";
|
||||
import { RouteConfig } from "@/router";
|
||||
import { DataStamp, ProjectLimits, ProjectsStorageBandwidthDaily } from "@/types/projects";
|
||||
import { Dimensions, Size } from "@/utils/bytesSize";
|
||||
|
||||
import VLoader from "@/components/common/VLoader.vue";
|
||||
import InfoContainer from "@/components/project/newProjectDashboard/InfoContainer.vue";
|
||||
import DashboardChart from "@/components/project/newProjectDashboard/DashboardChart.vue";
|
||||
import VButton from "@/components/common/VButton.vue";
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
components: {
|
||||
VLoader,
|
||||
VButton,
|
||||
InfoContainer,
|
||||
DashboardChart,
|
||||
}
|
||||
})
|
||||
export default class NewProjectDashboard extends Vue {
|
||||
public now = new Date().toLocaleDateString('en-US');
|
||||
public isDataFetching = true;
|
||||
|
||||
public chartWidth = 0;
|
||||
|
||||
public $refs: {
|
||||
dashboard: HTMLDivElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Fetches project limits.
|
||||
*/
|
||||
public async mounted(): Promise<void> {
|
||||
if (!this.$store.getters.selectedProject.id) {
|
||||
await this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.recalculateChartWidth)
|
||||
this.recalculateChartWidth();
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, new ProjectsStorageBandwidthDaily(this.chartTestData(), this.chartTestData()));
|
||||
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
|
||||
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
|
||||
|
||||
this.isDataFetching = false;
|
||||
} catch (error) {
|
||||
await this.$notify.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook before component destruction.
|
||||
* Removes event on window resizing.
|
||||
*/
|
||||
public beforeDestroy(): void {
|
||||
window.removeEventListener('resize', this.recalculateChartWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used container size recalculation for charts resizing.
|
||||
*/
|
||||
public recalculateChartWidth(): void {
|
||||
const fiftyPixels = 50;
|
||||
this.chartWidth = this.$refs.dashboard.getBoundingClientRect().width / 2 - fiftyPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds on upgrade button click logic.
|
||||
*/
|
||||
public onUpgradeClick(): void {
|
||||
this.$store.commit(PAYMENTS_MUTATIONS.TOGGLE_IS_ADD_PM_MODAL_SHOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds on create project button click logic.
|
||||
*/
|
||||
public onCreateProjectClick(): void {
|
||||
this.$router.push(RouteConfig.CreateProject.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds on upload button click logic.
|
||||
*/
|
||||
public onUploadClick(): void {
|
||||
this.$router.push(RouteConfig.Buckets.path).catch(() => {return;})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns formatted amount.
|
||||
*/
|
||||
public usedLimitFormatted(value: number): string {
|
||||
return this.formattedValue(new Size(value, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current limits from store.
|
||||
*/
|
||||
public get limits(): ProjectLimits {
|
||||
return this.$store.state.projectsModule.currentLimits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns status string based on account status.
|
||||
*/
|
||||
public get status(): string {
|
||||
return this.isProAccount ? 'Pro Account' : 'Free Account';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns pro account status from store.
|
||||
*/
|
||||
public get isProAccount(): boolean {
|
||||
return this.$store.getters.user.paidTier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns user's projects count from store.
|
||||
*/
|
||||
public get ownProjectsCount(): number {
|
||||
return this.$store.getters.projectsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* estimatedCharges returns estimated charges summary for selected project.
|
||||
*/
|
||||
public get estimatedCharges(): number {
|
||||
return this.$store.state.paymentsModule.priceSummaryForSelectedProject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns storage chart data from store.
|
||||
*/
|
||||
public get storageUsage(): DataStamp[] {
|
||||
return this.$store.state.projectsModule.storageChartData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bandwidth chart data from store.
|
||||
*/
|
||||
public get bandwidthUsage(): DataStamp[] {
|
||||
return this.$store.state.projectsModule.bandwidthChartData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats value to needed form and returns it.
|
||||
*/
|
||||
private formattedValue(value: Size): string {
|
||||
switch (value.label) {
|
||||
case Dimensions.Bytes:
|
||||
return '0';
|
||||
default:
|
||||
return `${value.formattedBytes.replace(/\\.0+$/, '')}${value.label}`;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove when backend is ready
|
||||
private chartTestData(): DataStamp[] {
|
||||
const startDate = new Date("2021-10-01");
|
||||
const endDate = new Date("2021-10-31");
|
||||
const arr = new Array<Date>();
|
||||
const dt = new Date(startDate);
|
||||
|
||||
while (dt <= endDate) {
|
||||
arr.push(new Date(dt));
|
||||
dt.setDate(dt.getDate() + 1);
|
||||
}
|
||||
|
||||
return arr.map(d => {
|
||||
return new DataStamp(Math.floor(Math.random() * 1000000000000), d);
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.project-dashboard {
|
||||
padding: 56px 40px;
|
||||
height: calc(100% - 112px);
|
||||
max-width: calc(100vw - 280px - 80px);
|
||||
background-image: url('../../../../static/images/project/background.png');
|
||||
background-position: top right;
|
||||
background-size: 70%;
|
||||
background-repeat: no-repeat;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
|
||||
&__loader {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #000;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
letter-spacing: -0.02em;
|
||||
color: #000;
|
||||
max-width: 365px;
|
||||
|
||||
&__value {
|
||||
text-decoration: underline;
|
||||
text-underline-position: under;
|
||||
text-decoration-color: #00e366;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload-button {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 65px 0 16px 0;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_Bold', sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 31px;
|
||||
letter-spacing: -0.02em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> *:last-child {
|
||||
margin-left: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__charts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__container {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 32px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 10px;
|
||||
|
||||
&__title {
|
||||
margin: 16px 0 2px 24px;
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&__loader {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin: 2px 0 0 24px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
|
||||
&__middle {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&__label,
|
||||
&__link {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&__link {
|
||||
text-decoration: underline !important;
|
||||
text-underline-position: under;
|
||||
|
||||
&:visited {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
|
||||
.project-dashboard {
|
||||
max-width: calc(100vw - 86px - 80px);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -32,7 +32,7 @@ import OldOverviewStep from '@/components/onboardingTour/steps/oldFlow/OldOvervi
|
||||
import CreateProject from '@/components/project/CreateProject.vue';
|
||||
import EditProjectDetails from '@/components/project/EditProjectDetails.vue';
|
||||
import ProjectDashboard from '@/components/project/ProjectDashboard.vue';
|
||||
import NewProjectDashboard from "@/components/project/NewProjectDashboard.vue";
|
||||
import NewProjectDashboard from "@/components/project/newProjectDashboard/NewProjectDashboard.vue";
|
||||
import ProjectsList from '@/components/projectsList/ProjectsList.vue';
|
||||
import ProjectMembersArea from '@/components/team/ProjectMembersArea.vue';
|
||||
import CLIInstall from "@/components/onboardingTour/steps/cliFlow/CLIInstall.vue";
|
||||
|
@ -3,17 +3,20 @@
|
||||
|
||||
import { StoreModule } from '@/store';
|
||||
import {
|
||||
DataStamp,
|
||||
Project,
|
||||
ProjectFields,
|
||||
ProjectLimits,
|
||||
ProjectsApi,
|
||||
ProjectsCursor,
|
||||
ProjectsPage,
|
||||
ProjectsStorageBandwidthDaily,
|
||||
} from '@/types/projects';
|
||||
|
||||
export const PROJECTS_ACTIONS = {
|
||||
FETCH: 'fetchProjects',
|
||||
FETCH_OWNED: 'fetchOwnedProjects',
|
||||
FETCH_DAILY_DATA: 'fetchDailyData',
|
||||
CREATE: 'createProject',
|
||||
CREATE_DEFAULT_PROJECT: 'createDefaultProject',
|
||||
SELECT: 'selectProject',
|
||||
@ -41,6 +44,7 @@ export const PROJECTS_MUTATIONS = {
|
||||
SET_TOTAL_LIMITS: 'SET_TOTAL_LIMITS',
|
||||
SET_PAGE_NUMBER: 'SET_PAGE_NUMBER',
|
||||
SET_PAGE: 'SET_PAGE',
|
||||
SET_DAILY_DATA: 'SET_DAILY_DATA',
|
||||
};
|
||||
|
||||
const defaultSelectedProject = new Project('', '', '', '', '', true, 0);
|
||||
@ -52,6 +56,8 @@ export class ProjectsState {
|
||||
public totalLimits: ProjectLimits = new ProjectLimits();
|
||||
public cursor: ProjectsCursor = new ProjectsCursor();
|
||||
public page: ProjectsPage = new ProjectsPage();
|
||||
public bandwidthChartData: DataStamp[] = [];
|
||||
public storageChartData: DataStamp[] = [];
|
||||
}
|
||||
|
||||
interface ProjectsContext {
|
||||
@ -67,6 +73,7 @@ interface ProjectsContext {
|
||||
|
||||
const {
|
||||
FETCH,
|
||||
FETCH_DAILY_DATA,
|
||||
CREATE,
|
||||
CREATE_DEFAULT_PROJECT,
|
||||
SELECT,
|
||||
@ -95,6 +102,7 @@ const {
|
||||
SET_TOTAL_LIMITS,
|
||||
SET_PAGE_NUMBER,
|
||||
SET_PAGE,
|
||||
SET_DAILY_DATA,
|
||||
} = PROJECTS_MUTATIONS;
|
||||
const projectsPageLimit = 7;
|
||||
|
||||
@ -174,6 +182,10 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState,
|
||||
[SET_PAGE](state: ProjectsState, page: ProjectsPage) {
|
||||
state.page = page;
|
||||
},
|
||||
[SET_DAILY_DATA](state: ProjectsState, payload: ProjectsStorageBandwidthDaily) {
|
||||
state.bandwidthChartData = payload.bandwidth;
|
||||
state.storageChartData = payload.storage;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[FETCH]: async function ({commit}: ProjectsContext): Promise<Project[]> {
|
||||
@ -191,6 +203,10 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState,
|
||||
|
||||
return projectsPage;
|
||||
},
|
||||
[FETCH_DAILY_DATA]: async function ({commit}: ProjectsContext, payload: ProjectsStorageBandwidthDaily): Promise<void> {
|
||||
// TODO: rework when backend is ready
|
||||
commit(SET_DAILY_DATA, payload);
|
||||
},
|
||||
[CREATE]: async function ({commit}: ProjectsContext, createProjectFields: ProjectFields): Promise<Project> {
|
||||
const project = await api.create(createProjectFields);
|
||||
|
||||
|
261
web/satellite/src/types/chart.ts
Normal file
261
web/satellite/src/types/chart.ts
Normal file
@ -0,0 +1,261 @@
|
||||
// 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,
|
||||
pointBorderColor: string,
|
||||
data: number[],
|
||||
) {
|
||||
this.labels = labels;
|
||||
|
||||
for (let i = 0; i < this.labels.length; i++) {
|
||||
this.datasets[i] = new DataSets(
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
pointBorderColor,
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DataSets class holds info for chart's DataSets entity.
|
||||
*/
|
||||
class DataSets {
|
||||
public constructor(
|
||||
public backgroundColor: string,
|
||||
public borderColor: string,
|
||||
public pointBorderColor: string,
|
||||
public data: number[],
|
||||
public borderWidth: number = 4,
|
||||
public pointHoverBackgroundColor: string = 'white',
|
||||
public pointHoverBorderWidth: number = 5,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* TooltipParams holds tooltip's configuration
|
||||
*/
|
||||
export class TooltipParams {
|
||||
public constructor(
|
||||
public tooltipModel: TooltipModel,
|
||||
public chartId: string,
|
||||
public tooltipId: string,
|
||||
public markUp: string,
|
||||
public tooltipTop: number,
|
||||
public tooltipLeft: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* StylingConstants holds tooltip styling constants
|
||||
*/
|
||||
class StylingConstants {
|
||||
public static tooltipOpacity = '1';
|
||||
public static tooltipPosition = 'absolute';
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling holds tooltip's styling configuration
|
||||
*/
|
||||
class Styling {
|
||||
public constructor(
|
||||
public tooltipModel: TooltipModel,
|
||||
public element: HTMLElement,
|
||||
public topPosition: number,
|
||||
public leftPosition: number,
|
||||
public chartPosition: ClientRect,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color is a color definition.
|
||||
*/
|
||||
export type Color = string
|
||||
|
||||
/**
|
||||
* TooltipItem contains datapoint information.
|
||||
*/
|
||||
export interface TooltipItem {
|
||||
// Label for the tooltip
|
||||
label: string,
|
||||
|
||||
// Value for the tooltip
|
||||
value: string,
|
||||
|
||||
// X Value of the tooltip
|
||||
// (deprecated) use `value` or `label` instead
|
||||
xLabel: number | string,
|
||||
|
||||
// Y value of the tooltip
|
||||
// (deprecated) use `value` or `label` instead
|
||||
yLabel: number | string,
|
||||
|
||||
// Index of the dataset the item comes from
|
||||
datasetIndex: number,
|
||||
|
||||
// Index of this data item in the dataset
|
||||
index: number,
|
||||
|
||||
// X position of matching point
|
||||
x: number,
|
||||
|
||||
// Y position of matching point
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* TooltipModel contains parameters that can be used to render the tooltip.
|
||||
*/
|
||||
export interface TooltipModel {
|
||||
// The items that we are rendering in the tooltip. See Tooltip Item Interface section
|
||||
dataPoints: TooltipItem[],
|
||||
|
||||
// Positioning
|
||||
xPadding: number,
|
||||
yPadding: number,
|
||||
xAlign: string,
|
||||
yAlign: string,
|
||||
|
||||
// X and Y properties are the top left of the tooltip
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
// Where the tooltip points to
|
||||
caretX: number,
|
||||
caretY: number,
|
||||
|
||||
// Body
|
||||
// The body lines that need to be rendered
|
||||
// Each object contains 3 parameters
|
||||
// before: string[] // lines of text before the line with the color square
|
||||
// lines: string[], // lines of text to render as the main item with color square
|
||||
// after: string[], // lines of text to render after the main lines
|
||||
body: {before: string[]; lines: string[], after: string[]}[],
|
||||
// lines of text that appear after the title but before the body
|
||||
beforeBody: string[],
|
||||
// line of text that appear after the body and before the footer
|
||||
afterBody: string[],
|
||||
bodyFontColor: Color,
|
||||
_bodyFontFamily: string,
|
||||
_bodyFontStyle: string,
|
||||
_bodyAlign: string,
|
||||
bodyFontSize: number,
|
||||
bodySpacing: number,
|
||||
|
||||
// Title
|
||||
// lines of text that form the title
|
||||
title: string[],
|
||||
titleFontColor: Color,
|
||||
_titleFontFamily: string,
|
||||
_titleFontStyle: string,
|
||||
titleFontSize: number,
|
||||
_titleAlign: string,
|
||||
titleSpacing: number,
|
||||
titleMarginBottom: number,
|
||||
|
||||
// Footer
|
||||
// lines of text that form the footer
|
||||
footer: string[],
|
||||
footerFontColor: Color,
|
||||
_footerFontFamily: string,
|
||||
_footerFontStyle: string,
|
||||
footerFontSize: number,
|
||||
_footerAlign: string,
|
||||
footerSpacing: number,
|
||||
footerMarginTop: number,
|
||||
|
||||
// Appearance
|
||||
caretSize: number,
|
||||
caretPadding: number,
|
||||
cornerRadius: number,
|
||||
backgroundColor: Color,
|
||||
|
||||
// colors to render for each item in body[]. This is the color of the squares in the tooltip
|
||||
labelColors: Color[],
|
||||
labelTextColors: Color[],
|
||||
|
||||
// 0 opacity is a hidden tooltip
|
||||
opacity: number,
|
||||
legendColorBackground: Color,
|
||||
displayColors: boolean,
|
||||
borderColor: Color,
|
||||
borderWidth: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (!params.tooltipModel.opacity) {
|
||||
Tooltip.remove(tooltip);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 remove(tooltipEl: HTMLElement) {
|
||||
document.body.removeChild(tooltipEl);
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RenderChart contains definition for renderChart and addPlugin, that can be used to cast
|
||||
* a derived chart type, with `(this as unknown as RenderChart).renderChart`
|
||||
*/
|
||||
export interface RenderChart {
|
||||
renderChart<A, B>(A, B): void
|
||||
addPlugin (plugin?: Record<string, (chart) => void>): void
|
||||
}
|
@ -23,8 +23,8 @@ export interface ProjectsApi {
|
||||
* Update project name and description.
|
||||
*
|
||||
* @param projectId - project ID
|
||||
* @param name - project name
|
||||
* @param description - project description
|
||||
* @param updateProjectFields - project fields to update
|
||||
* @param updateProjectLimits - project limits to update
|
||||
* @returns Project[]
|
||||
* @throws Error
|
||||
*/
|
||||
@ -179,3 +179,39 @@ export class ProjectsCursor {
|
||||
public page: number = 0,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* DataStamp is storage/bandwidth usage stamp for satellite at some point in time
|
||||
*/
|
||||
export class DataStamp {
|
||||
public value: number;
|
||||
public intervalStart: Date;
|
||||
|
||||
public constructor(value = 0, intervalStart: Date = new Date()) {
|
||||
this.value = value;
|
||||
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): DataStamp {
|
||||
const now = new Date();
|
||||
now.setUTCDate(date);
|
||||
now.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
return new DataStamp(0, now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ProjectsStorageBandwidthDaily is used to describe project's daily storage and bandwidth usage.
|
||||
*/
|
||||
export class ProjectsStorageBandwidthDaily {
|
||||
public constructor(
|
||||
public bandwidth: DataStamp[] = [],
|
||||
public storage: DataStamp[] = [],
|
||||
) {}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export enum Memory {
|
||||
}
|
||||
|
||||
export enum Dimensions {
|
||||
Bytes = 'Bytes',
|
||||
Bytes = 'B',
|
||||
KB = 'KB',
|
||||
MB = 'MB',
|
||||
GB = 'GB',
|
||||
|
27
web/satellite/src/utils/chart.ts
Normal file
27
web/satellite/src/utils/chart.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
export class ChartUtils {
|
||||
/**
|
||||
* Used to display correct number of days on chart's labels.
|
||||
*
|
||||
* @returns daysDisplayed - array of days converted to a string by using the current locale
|
||||
*/
|
||||
public static daysDisplayedOnChart(start: Date, end: Date): string[] {
|
||||
const arr = Array<string>();
|
||||
|
||||
if (start === end) {
|
||||
arr.push(`${start.getMonth() + 1}/${start.getDate()}`);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
const dt = start;
|
||||
while (dt <= end) {
|
||||
arr.push(`${dt.getMonth() + 1}/${dt.getDate()}`);
|
||||
dt.setDate(dt.getDate() + 1)
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
@ -2,9 +2,8 @@
|
||||
|
||||
exports[`NoBucketsArea.vue renders correctly 1`] = `
|
||||
<div class="no-buckets-area"><img src="@/../static/images/buckets/bucket.png" alt="bucket image" class="no-buckets-area__image">
|
||||
<h2 class="no-buckets-area__message">Create your first bucket to get started.</h2> <a href="https://docs.storj.io/api-reference/uplink-cli" target="_blank" rel="noopener noreferrer" class="no-buckets-area__first-button">
|
||||
Get Started
|
||||
</a> <a href="https://docs.storj.io/" target="_blank" rel="noopener noreferrer" class="no-buckets-area__second-button">
|
||||
<h2 class="no-buckets-area__message">Create your first bucket to get started.</h2>
|
||||
<vbutton-stub label="Get Started" width="156px" height="47px" borderradius="6px" onpress="function () { [native code] }"></vbutton-stub> <a href="https://docs.storj.io/" target="_blank" rel="noopener noreferrer" class="no-buckets-area__second-button">
|
||||
Visit the Docs
|
||||
</a>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user