satellitedb/projectaccounting, web/satellite: reworked bandwidth chart to show both allocated and settled bandwidth
Extended DB query. Reworked charts UI functionality to show 2 lines if necessary. Change-Id: I8ac4e4fa07676fc9fa7e9c078ecdeed62233b8e2
This commit is contained in:
parent
2d3c417c92
commit
150be885b7
@ -93,8 +93,9 @@ type ProjectLimits struct {
|
||||
|
||||
// ProjectDailyUsage holds project daily usage.
|
||||
type ProjectDailyUsage struct {
|
||||
StorageUsage []ProjectUsageByDay `json:"storageUsage"`
|
||||
BandwidthUsage []ProjectUsageByDay `json:"bandwidthUsage"`
|
||||
StorageUsage []ProjectUsageByDay `json:"storageUsage"`
|
||||
AllocatedBandwidthUsage []ProjectUsageByDay `json:"allocatedBandwidthUsage"`
|
||||
SettledBandwidthUsage []ProjectUsageByDay `json:"settledBandwidthUsage"`
|
||||
}
|
||||
|
||||
// ProjectUsageByDay holds project daily usage.
|
||||
@ -208,7 +209,7 @@ type ProjectAccounting interface {
|
||||
GetProjectDailyBandwidth(ctx context.Context, projectID uuid.UUID, year int, month time.Month, day int) (int64, int64, int64, error)
|
||||
// DeleteProjectBandwidthBefore deletes project bandwidth rollups before the given time
|
||||
DeleteProjectBandwidthBefore(ctx context.Context, before time.Time) error
|
||||
// GetProjectDailyUsageByDateRange returns daily allocated bandwidth and storage usage for the specified date range.
|
||||
// GetProjectDailyUsageByDateRange returns daily allocated, settled bandwidth and storage usage for the specified date range.
|
||||
GetProjectDailyUsageByDateRange(ctx context.Context, projectID uuid.UUID, from, to time.Time, crdbInterval time.Duration) (*ProjectDailyUsage, error)
|
||||
|
||||
// UpdateProjectUsageLimit updates project usage limit.
|
||||
|
@ -5,17 +5,22 @@ package consoleapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
@ -113,3 +118,116 @@ func Test_TotalUsageLimits(t *testing.T) {
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func Test_DailyUsage(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 1, UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Console.OpenRegistrationEnabled = true
|
||||
config.Console.RateLimit.Burst = 10
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
const (
|
||||
bucketName = "testbucket"
|
||||
firstPath = "path"
|
||||
secondPath = "another_path"
|
||||
)
|
||||
|
||||
now := time.Now()
|
||||
inFiveMinutes := time.Now().Add(5 * time.Minute)
|
||||
|
||||
var (
|
||||
satelliteSys = planet.Satellites[0]
|
||||
uplink = planet.Uplinks[0]
|
||||
projectID = uplink.Projects[0].ID
|
||||
since = strconv.FormatInt(now.Unix(), 10)
|
||||
before = strconv.FormatInt(inFiveMinutes.Unix(), 10)
|
||||
)
|
||||
|
||||
newUser := console.CreateUser{
|
||||
FullName: "Daily Usage Test",
|
||||
ShortName: "",
|
||||
Email: "du@test.test",
|
||||
}
|
||||
|
||||
user, err := satelliteSys.AddUser(ctx, newUser, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = satelliteSys.DB.Console().ProjectMembers().Insert(ctx, user.ID, projectID)
|
||||
require.NoError(t, err)
|
||||
|
||||
planet.Satellites[0].Orders.Chore.Loop.Pause()
|
||||
satelliteSys.Accounting.Tally.Loop.Pause()
|
||||
|
||||
usage, err := satelliteSys.DB.ProjectAccounting().GetProjectDailyUsageByDateRange(ctx, projectID, now, inFiveMinutes, 0)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, len(usage.AllocatedBandwidthUsage))
|
||||
require.Zero(t, len(usage.SettledBandwidthUsage))
|
||||
require.Zero(t, len(usage.StorageUsage))
|
||||
|
||||
firstSegment := testrand.Bytes(5 * memory.KiB)
|
||||
secondSegment := testrand.Bytes(10 * memory.KiB)
|
||||
|
||||
err = uplink.Upload(ctx, satelliteSys, bucketName, firstPath, firstSegment)
|
||||
require.NoError(t, err)
|
||||
err = uplink.Upload(ctx, satelliteSys, bucketName, secondPath, secondSegment)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = uplink.Download(ctx, satelliteSys, bucketName, firstPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, planet.WaitForStorageNodeEndpoints(ctx))
|
||||
tomorrow := time.Now().Add(24 * time.Hour)
|
||||
planet.StorageNodes[0].Storage2.Orders.SendOrders(ctx, tomorrow)
|
||||
|
||||
planet.Satellites[0].Orders.Chore.Loop.TriggerWait()
|
||||
satelliteSys.Accounting.Tally.Loop.TriggerWait()
|
||||
|
||||
// we are using full name as a password
|
||||
token, err := satelliteSys.API.Console.Service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
||||
require.NoError(t, err)
|
||||
|
||||
client := http.DefaultClient
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
"GET",
|
||||
fmt.Sprintf("http://%s/api/v0/projects/%s/daily-usage?from=%s&to=%s", planet.Satellites[0].API.Console.Listener.Addr().String(), projectID.String(), since, before),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
expire := time.Now().AddDate(0, 0, 1)
|
||||
cookie := http.Cookie{
|
||||
Name: "_tokenKey",
|
||||
Path: "/",
|
||||
Value: token,
|
||||
Expires: expire,
|
||||
}
|
||||
|
||||
req.AddCookie(&cookie)
|
||||
|
||||
result, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(result.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output accounting.ProjectDailyUsage
|
||||
|
||||
err = json.Unmarshal(body, &output)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.GreaterOrEqual(t, output.StorageUsage[0].Value, 15*memory.KiB)
|
||||
require.GreaterOrEqual(t, output.AllocatedBandwidthUsage[0].Value, 5*memory.KiB)
|
||||
require.GreaterOrEqual(t, output.SettledBandwidthUsage[0].Value, 5*memory.KiB)
|
||||
|
||||
defer func() {
|
||||
err = result.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ func (db *ProjectAccounting) GetProjectDailyBandwidth(ctx context.Context, proje
|
||||
return allocated, settled, dead, err
|
||||
}
|
||||
|
||||
// GetProjectDailyUsageByDateRange returns project daily allocated bandwidth and storage usage by specific date range.
|
||||
// GetProjectDailyUsageByDateRange returns project daily allocated, settled bandwidth and storage usage by specific date range.
|
||||
func (db *ProjectAccounting) GetProjectDailyUsageByDateRange(ctx context.Context, projectID uuid.UUID, from, to time.Time, crdbInterval time.Duration) (_ *accounting.ProjectDailyUsage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
@ -209,13 +209,14 @@ func (db *ProjectAccounting) GetProjectDailyUsageByDateRange(ctx context.Context
|
||||
endOfDay := time.Date(to.Year(), to.Month(), to.Day(), 23, 59, 59, 0, time.UTC)
|
||||
|
||||
allocatedBandwidth := make([]accounting.ProjectUsageByDay, 0)
|
||||
settledBandwidth := make([]accounting.ProjectUsageByDay, 0)
|
||||
storage := make([]accounting.ProjectUsageByDay, 0)
|
||||
|
||||
err = pgxutil.Conn(ctx, db.db, func(conn *pgx.Conn) error {
|
||||
var batch pgx.Batch
|
||||
|
||||
batch.Queue(db.db.Rebind(`
|
||||
SELECT interval_day, COALESCE(egress_allocated, 0)
|
||||
SELECT interval_day, egress_allocated, egress_settled
|
||||
FROM project_bandwidth_daily_rollups
|
||||
WHERE project_id = $1 AND (interval_day BETWEEN $2 AND $3)
|
||||
`), projectID, from, endOfDay)
|
||||
@ -255,45 +256,74 @@ func (db *ProjectAccounting) GetProjectDailyUsageByDateRange(ctx context.Context
|
||||
results := conn.SendBatch(ctx, &batch)
|
||||
defer func() { err = errs.Combine(err, results.Close()) }()
|
||||
|
||||
handleResult := func(results pgx.BatchResults, data *[]accounting.ProjectUsageByDay) error {
|
||||
rows, err := results.Query()
|
||||
bandwidthRows, err := results.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for bandwidthRows.Next() {
|
||||
var day time.Time
|
||||
var allocated int64
|
||||
var settled int64
|
||||
|
||||
err = bandwidthRows.Scan(&day, &allocated, &settled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var day time.Time
|
||||
var amount int64
|
||||
allocatedBandwidth = append(allocatedBandwidth, accounting.ProjectUsageByDay{
|
||||
Date: day.UTC(),
|
||||
Value: allocated,
|
||||
})
|
||||
|
||||
err = rows.Scan(&day, &amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*data = append(*data, accounting.ProjectUsageByDay{
|
||||
Date: day,
|
||||
Value: amount,
|
||||
})
|
||||
}
|
||||
|
||||
defer func() { rows.Close() }()
|
||||
|
||||
return rows.Err()
|
||||
settledBandwidth = append(settledBandwidth, accounting.ProjectUsageByDay{
|
||||
Date: day.UTC(),
|
||||
Value: settled,
|
||||
})
|
||||
}
|
||||
|
||||
var errlist errs.Group
|
||||
errlist.Add(handleResult(results, &allocatedBandwidth))
|
||||
errlist.Add(handleResult(results, &storage))
|
||||
defer func() { bandwidthRows.Close() }()
|
||||
err = bandwidthRows.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return errlist.Err()
|
||||
storageRows, err := results.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for storageRows.Next() {
|
||||
var day time.Time
|
||||
var amount int64
|
||||
|
||||
err = storageRows.Scan(&day, &amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storage = append(storage, accounting.ProjectUsageByDay{
|
||||
Date: day.UTC(),
|
||||
Value: amount,
|
||||
})
|
||||
}
|
||||
|
||||
defer func() { storageRows.Close() }()
|
||||
err = storageRows.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, Error.New("unable to get project daily usage: %w", err)
|
||||
}
|
||||
|
||||
return &accounting.ProjectDailyUsage{
|
||||
StorageUsage: storage,
|
||||
BandwidthUsage: allocatedBandwidth,
|
||||
StorageUsage: storage,
|
||||
AllocatedBandwidthUsage: allocatedBandwidth,
|
||||
SettledBandwidthUsage: settledBandwidth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,8 @@ func Test_DailyUsage(t *testing.T) {
|
||||
|
||||
usage0, err := satelliteSys.DB.ProjectAccounting().GetProjectDailyUsageByDateRange(ctx, projectID, now, inFiveMinutes, 0)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, len(usage0.BandwidthUsage))
|
||||
require.Zero(t, len(usage0.AllocatedBandwidthUsage))
|
||||
require.Zero(t, len(usage0.SettledBandwidthUsage))
|
||||
require.Zero(t, len(usage0.StorageUsage))
|
||||
|
||||
firstSegment := testrand.Bytes(5 * memory.KiB)
|
||||
@ -75,7 +76,8 @@ func Test_DailyUsage(t *testing.T) {
|
||||
usage1, err := satelliteSys.DB.ProjectAccounting().GetProjectDailyUsageByDateRange(ctx, projectID, now, inFiveMinutes, 0)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, usage1.StorageUsage[0].Value, 15*memory.KiB)
|
||||
require.GreaterOrEqual(t, usage1.BandwidthUsage[0].Value, 5*memory.KiB)
|
||||
require.GreaterOrEqual(t, usage1.AllocatedBandwidthUsage[0].Value, 5*memory.KiB)
|
||||
require.GreaterOrEqual(t, usage1.SettledBandwidthUsage[0].Value, 5*memory.KiB)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -200,11 +200,6 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
||||
* throws Error
|
||||
*/
|
||||
public async getDailyUsage(projectId: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily> {
|
||||
// Set date range to be in UTC format.
|
||||
start.setUTCDate(start.getDate());
|
||||
start.setUTCHours(0, 0, 0, 0);
|
||||
end.setUTCDate(end.getDate());
|
||||
end.setUTCHours(0, 0, 0, 0);
|
||||
const since = Time.toUnixTimestamp(start).toString();
|
||||
const before = Time.toUnixTimestamp(end).toString();
|
||||
const path = `${this.ROOT_PATH}/${projectId}/daily-usage?from=${since}&to=${before}`;
|
||||
@ -214,14 +209,17 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
||||
const usage = await response.json();
|
||||
|
||||
return new ProjectsStorageBandwidthDaily(
|
||||
usage.bandwidthUsage.map(el => {
|
||||
// Set the timestamps to be the beginning of the day.
|
||||
usage.storageUsage.map(el => {
|
||||
const date = new Date(el.date)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return new DataStamp(el.value, date)
|
||||
}),
|
||||
usage.storageUsage.map(el => {
|
||||
// Set the timestamps to be the beginning of the day.
|
||||
usage.allocatedBandwidthUsage.map(el => {
|
||||
const date = new Date(el.date)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return new DataStamp(el.value, date)
|
||||
}),
|
||||
usage.settledBandwidthUsage.map(el => {
|
||||
const date = new Date(el.date)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return new DataStamp(el.value, date)
|
||||
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<hr class="duration-picker__break">
|
||||
<div class="duration-picker__wrapper">
|
||||
<VDateRangePicker :on-date-pick="onCustomRangePick" is-open="true" />
|
||||
<VDateRangePicker :on-date-pick="onCustomRangePick" :is-open="true" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -100,7 +100,7 @@ export default class VChart extends Vue {
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: this.chartData.datasets.length === 1 ? 10 : 0,
|
||||
radius: this.chartData.labels.length === 1 ? 10 : 0,
|
||||
hoverRadius: 10,
|
||||
hitRadius: 8,
|
||||
},
|
||||
@ -125,6 +125,7 @@ export default class VChart extends Vue {
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
axis: 'x',
|
||||
custom: (tooltipModel) => {
|
||||
this.tooltipConstructor(tooltipModel);
|
||||
},
|
||||
|
@ -76,11 +76,6 @@ export default class VInfo extends Vue {
|
||||
filter: drop-shadow(0 0 34px #0a1b2c47);
|
||||
z-index: 1;
|
||||
|
||||
&__click-mock {
|
||||
height: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
background-color: white;
|
||||
width: 40px;
|
||||
|
@ -6,7 +6,7 @@
|
||||
<NoBucketArea v-if="isNoBucketAreaShown" />
|
||||
<div v-else class="buckets-overflow">
|
||||
<div class="buckets-header">
|
||||
<p class="buckets-header__title">Buckets</p>
|
||||
<p class="buckets-header__title">Usage per bucket</p>
|
||||
<VHeader
|
||||
class="buckets-header-component"
|
||||
placeholder="Buckets"
|
||||
@ -182,6 +182,7 @@ export default class BucketArea extends Vue {
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: #1b2533;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<VChart
|
||||
id="bandwidth-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, ChartTooltipData } from '@/types/chart';
|
||||
import { DataStamp } from "@/types/projects";
|
||||
import { ChartUtils } from "@/utils/chart";
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
components: { VChart }
|
||||
})
|
||||
export default class BandwidthChart extends BaseChart {
|
||||
@Prop({default: () => []})
|
||||
public readonly settledData: DataStamp[];
|
||||
@Prop({default: () => []})
|
||||
public readonly allocatedData: DataStamp[];
|
||||
@Prop({default: new Date()})
|
||||
public readonly since: Date;
|
||||
@Prop({default: new Date()})
|
||||
public readonly before: Date;
|
||||
|
||||
/**
|
||||
* Returns formatted data to render chart.
|
||||
*/
|
||||
public get chartData(): ChartData {
|
||||
const mainData: number[] = this.settledData.map(el => el.value);
|
||||
const secondaryData: number[] = this.allocatedData.map(el => el.value)
|
||||
const xAxisDateLabels: string[] = ChartUtils.daysDisplayedOnChart(this.since, this.before);
|
||||
|
||||
return new ChartData(
|
||||
xAxisDateLabels,
|
||||
"#FFE0E7",
|
||||
"#EE86AD",
|
||||
"#FF458B",
|
||||
mainData,
|
||||
"#FFF6F8",
|
||||
"#FFC0CF",
|
||||
"#FFC0CF",
|
||||
secondaryData,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as constructor of custom tooltip.
|
||||
*/
|
||||
public tooltip(tooltipModel: TooltipModel): void {
|
||||
if (!tooltipModel.dataPoints) {
|
||||
const settledTooltip = Tooltip.createTooltip('settled-bandwidth-tooltip')
|
||||
const allocatedTooltip = Tooltip.createTooltip('allocated-bandwidth-tooltip')
|
||||
Tooltip.remove(settledTooltip)
|
||||
Tooltip.remove(allocatedTooltip)
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
tooltipModel.dataPoints.forEach(p => {
|
||||
let tooltipParams: TooltipParams;
|
||||
if (p.datasetIndex === 0) {
|
||||
tooltipParams = new TooltipParams(tooltipModel, 'bandwidth-chart', 'settled-bandwidth-tooltip',
|
||||
this.settledTooltipMarkUp(tooltipModel), -20, 78);
|
||||
} else {
|
||||
tooltipParams = new TooltipParams(tooltipModel, 'bandwidth-chart', 'allocated-bandwidth-tooltip',
|
||||
this.allocatedTooltipMarkUp(tooltipModel), 95, 78);
|
||||
}
|
||||
|
||||
Tooltip.custom(tooltipParams);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns allocated bandwidth tooltip's html mark up.
|
||||
*/
|
||||
private allocatedTooltipMarkUp(tooltipModel: TooltipModel): string {
|
||||
if (!tooltipModel.dataPoints) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dataIndex = tooltipModel.dataPoints[0].index;
|
||||
const dataPoint = new ChartTooltipData(this.allocatedData[dataIndex]);
|
||||
|
||||
return `<div class='allocated-tooltip'>
|
||||
<p class='settled-tooltip__title'>Allocated</p>
|
||||
<p class='allocated-tooltip__value'>${dataPoint.date}<b class='allocated-tooltip__value__bold'> / ${dataPoint.value}</b></p>
|
||||
<div class='allocated-tooltip__arrow'></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns settled bandwidth tooltip's html mark up.
|
||||
*/
|
||||
private settledTooltipMarkUp(tooltipModel: TooltipModel): string {
|
||||
if (!tooltipModel.dataPoints) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dataIndex = tooltipModel.dataPoints[0].index;
|
||||
const dataPoint = new ChartTooltipData(this.settledData[dataIndex]);
|
||||
|
||||
return `<div class='settled-tooltip'>
|
||||
<div class='settled-tooltip__arrow'></div>
|
||||
<p class='settled-tooltip__title'>Settled</p>
|
||||
<p class='settled-tooltip__value'>${dataPoint.date}<b class='settled-tooltip__value__bold'> / ${dataPoint.value}</b></p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settled-tooltip,
|
||||
.allocated-tooltip {
|
||||
margin: 8px;
|
||||
position: relative;
|
||||
border-radius: 14.5px;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 120px;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
color: #fff;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 14px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
align-self: flex-start;
|
||||
|
||||
&__bold {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.settled-tooltip {
|
||||
background-color: #ff458b;
|
||||
padding: 4px 10px 8px;
|
||||
|
||||
&__arrow {
|
||||
margin: -12px 0 4px;
|
||||
border-radius: 0 0 0 8px;
|
||||
transform: scale(1, 0.85) translate(0, 20%) rotate(-45deg);
|
||||
background-color: #ff458b;
|
||||
}
|
||||
}
|
||||
|
||||
.allocated-tooltip {
|
||||
background-color: #ee86ad;
|
||||
padding: 8px 10px 0;
|
||||
|
||||
&__arrow {
|
||||
margin-bottom: -4px;
|
||||
border-radius: 8px 0 0 0;
|
||||
transform: scale(1, 0.85) translate(0, 20%) rotate(45deg);
|
||||
background-color: #ee86ad;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -60,42 +60,63 @@
|
||||
</div>
|
||||
<div class="project-dashboard__charts">
|
||||
<div class="project-dashboard__charts__container">
|
||||
<h3 class="project-dashboard__charts__container__title">Storage</h3>
|
||||
<div class="project-dashboard__charts__container__header">
|
||||
<h3 class="project-dashboard__charts__container__header__title">Storage</h3>
|
||||
</div>
|
||||
<p class="project-dashboard__charts__container__info">
|
||||
This is your total storage used per day
|
||||
</p>
|
||||
<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"
|
||||
<StorageChart
|
||||
:width="chartWidth"
|
||||
:height="170"
|
||||
:data="storageUsage"
|
||||
:since="chartsSinceDate"
|
||||
:before="chartsBeforeDate"
|
||||
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>
|
||||
<div class="project-dashboard__charts__container__header">
|
||||
<h3 class="project-dashboard__charts__container__header__title">Bandwidth</h3>
|
||||
<div class="project-dashboard__charts__container__header__right">
|
||||
<span class="project-dashboard__charts__container__header__right__allocated-color" />
|
||||
<p class="project-dashboard__charts__container__header__right__allocated-label">Allocated</p>
|
||||
<span class="project-dashboard__charts__container__header__right__settled-color" />
|
||||
<p class="project-dashboard__charts__container__header__right__settled-label">Settled</p>
|
||||
<VInfo class="project-dashboard__charts__container__header__right__info">
|
||||
<template #icon>
|
||||
<InfoIcon />
|
||||
</template>
|
||||
<template #message>
|
||||
<p class="project-dashboard__charts__container__header__right__info__message">
|
||||
The bandwidth allocated takes few hours to be settled.
|
||||
<a
|
||||
class="project-dashboard__charts__container__header__right__info__message__link"
|
||||
href="https://docs.storj.io/dcs/billing-payment-and-accounts-1/pricing/billing-and-payment#bandwidth-fee"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</VInfo>
|
||||
</div>
|
||||
</div>
|
||||
<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) }}
|
||||
This is your bandwidth usage per day
|
||||
</p>
|
||||
<DashboardChart
|
||||
name="bandwidth"
|
||||
<BandwidthChart
|
||||
:width="chartWidth"
|
||||
:height="170"
|
||||
:data="bandwidthUsage"
|
||||
:settled-data="settledBandwidthUsage"
|
||||
:allocated-data="allocatedBandwidthUsage"
|
||||
:since="chartsSinceDate"
|
||||
:before="chartsBeforeDate"
|
||||
background-color="#FFE0E7"
|
||||
border-color="#FFC0CF"
|
||||
point-border-color="#FF458B"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@ -162,12 +183,15 @@ import { ChartUtils } from "@/utils/chart";
|
||||
|
||||
import VLoader from "@/components/common/VLoader.vue";
|
||||
import InfoContainer from "@/components/project/newProjectDashboard/InfoContainer.vue";
|
||||
import DashboardChart from "@/components/project/newProjectDashboard/DashboardChart.vue";
|
||||
import StorageChart from "@/components/project/newProjectDashboard/StorageChart.vue";
|
||||
import BandwidthChart from "@/components/project/newProjectDashboard/BandwidthChart.vue";
|
||||
import VButton from "@/components/common/VButton.vue";
|
||||
import DateRangeSelection from "@/components/project/newProjectDashboard/DateRangeSelection.vue";
|
||||
import VInfo from "@/components/common/VInfo.vue";
|
||||
import BucketArea from '@/components/project/buckets/BucketArea.vue';
|
||||
|
||||
import NewProjectIcon from "@/../static/images/project/newProject.svg";
|
||||
import InfoIcon from '@/../static/images/project/infoIcon.svg';
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
@ -175,9 +199,12 @@ import NewProjectIcon from "@/../static/images/project/newProject.svg";
|
||||
VLoader,
|
||||
VButton,
|
||||
InfoContainer,
|
||||
DashboardChart,
|
||||
StorageChart,
|
||||
BandwidthChart,
|
||||
DateRangeSelection,
|
||||
VInfo,
|
||||
NewProjectIcon,
|
||||
InfoIcon,
|
||||
BucketArea,
|
||||
}
|
||||
})
|
||||
@ -289,8 +316,11 @@ export default class NewProjectDashboard extends Vue {
|
||||
* @param dateRange
|
||||
*/
|
||||
public async onChartsDateRangePick(dateRange: Date[]): Promise<void> {
|
||||
const since = new Date(dateRange[0])
|
||||
const before = new Date(dateRange[1])
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, {since: dateRange[0], before: dateRange[1]})
|
||||
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, {since, before})
|
||||
} catch (error) {
|
||||
await this.$notify.error(error.message);
|
||||
}
|
||||
@ -346,10 +376,17 @@ export default class NewProjectDashboard extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bandwidth chart data from store.
|
||||
* Returns settled bandwidth chart data from store.
|
||||
*/
|
||||
public get bandwidthUsage(): DataStamp[] {
|
||||
return ChartUtils.populateEmptyUsage(this.$store.state.projectsModule.bandwidthChartData, this.chartsSinceDate, this.chartsBeforeDate);
|
||||
public get settledBandwidthUsage(): DataStamp[] {
|
||||
return ChartUtils.populateEmptyUsage(this.$store.state.projectsModule.settledBandwidthChartData, this.chartsSinceDate, this.chartsBeforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns allocated bandwidth chart data from store.
|
||||
*/
|
||||
public get allocatedBandwidthUsage(): DataStamp[] {
|
||||
return ChartUtils.populateEmptyUsage(this.$store.state.projectsModule.allocatedBandwidthChartData, this.chartsSinceDate, this.chartsBeforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -456,12 +493,76 @@ export default class NewProjectDashboard extends Vue {
|
||||
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;
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
&__title {
|
||||
margin: 16px 0 2px 24px;
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16px 16px 0 0;
|
||||
|
||||
&__allocated-color,
|
||||
&__settled-color {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__allocated-color {
|
||||
background: #ffc0cf;
|
||||
}
|
||||
|
||||
&__settled-color {
|
||||
background: #ff458b;
|
||||
}
|
||||
|
||||
&__allocated-label,
|
||||
&__settled-label {
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
color: #000;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&__allocated-label {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&__settled-label {
|
||||
margin-right: 11px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
cursor: pointer;
|
||||
max-height: 20px;
|
||||
|
||||
&__message {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
&__link {
|
||||
text-decoration: underline !important;
|
||||
color: #fff;
|
||||
|
||||
&:visited {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__loader {
|
||||
@ -524,6 +625,26 @@ export default class NewProjectDashboard extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .info__box {
|
||||
width: 180px;
|
||||
left: calc(50% - 20px);
|
||||
top: calc(100% + 1px);
|
||||
cursor: default;
|
||||
|
||||
&__message {
|
||||
background: #56606d;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
background: #56606d;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 0 0 -2px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
|
||||
.project-dashboard {
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<template>
|
||||
<VChart
|
||||
:id="`${name}-chart`"
|
||||
id="storage-chart"
|
||||
:key="chartKey"
|
||||
:chart-data="chartData"
|
||||
:width="width"
|
||||
@ -18,40 +18,17 @@ 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 { ChartData, Tooltip, TooltipParams, TooltipModel, ChartTooltipData } from '@/types/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}`;
|
||||
}
|
||||
}
|
||||
import { ChartUtils } from "@/utils/chart";
|
||||
|
||||
// @vue/component
|
||||
@Component({
|
||||
components: { VChart }
|
||||
})
|
||||
export default class DashboardChart extends BaseChart {
|
||||
export default class StorageChart 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;
|
||||
@Prop({default: new Date()})
|
||||
public readonly since: Date;
|
||||
@Prop({default: new Date()})
|
||||
@ -62,13 +39,13 @@ export default class DashboardChart extends BaseChart {
|
||||
*/
|
||||
public get chartData(): ChartData {
|
||||
const data: number[] = this.data.map(el => el.value)
|
||||
const xAxisDateLabels: string[] = this.daysDisplayedOnChart();
|
||||
const xAxisDateLabels: string[] = ChartUtils.daysDisplayedOnChart(this.since, this.before);
|
||||
|
||||
return new ChartData(
|
||||
xAxisDateLabels,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.pointBorderColor,
|
||||
"#E6EDF7",
|
||||
"#D7E8FF",
|
||||
"#003DC1",
|
||||
data,
|
||||
);
|
||||
}
|
||||
@ -77,36 +54,12 @@ export default class DashboardChart extends BaseChart {
|
||||
* Used as constructor of custom tooltip.
|
||||
*/
|
||||
public tooltip(tooltipModel: TooltipModel): void {
|
||||
const tooltipParams = new TooltipParams(tooltipModel, `${this.name}-chart`, `${this.name}-tooltip`,
|
||||
const tooltipParams = new TooltipParams(tooltipModel, 'storage-chart', 'storage-tooltip',
|
||||
this.tooltipMarkUp(tooltipModel), 76, 81);
|
||||
|
||||
Tooltip.custom(tooltipParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to display correct number of data points on chart.
|
||||
*/
|
||||
public daysDisplayedOnChart(): string[] {
|
||||
const since = new Date(this.since);
|
||||
// Create an array of future displayed data points.
|
||||
const arr = Array<string>();
|
||||
|
||||
// If there is only one day chosen in date picker then we fill array with only one data point label.
|
||||
if (since.getTime() === this.before.getTime()) {
|
||||
arr.push(`${since.getMonth() + 1}/${since.getDate()}`);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Fill the data points array with correct data points labels.
|
||||
while (since <= this.before) {
|
||||
arr.push(`${since.getMonth() + 1}/${since.getDate()}`);
|
||||
since.setDate(since.getDate() + 1)
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tooltip's html mark up.
|
||||
*/
|
||||
@ -116,11 +69,11 @@ export default class DashboardChart extends BaseChart {
|
||||
}
|
||||
|
||||
const dataIndex = tooltipModel.dataPoints[0].index;
|
||||
const dataPoint = new ChartTooltip(this.data[dataIndex]);
|
||||
const dataPoint = new ChartTooltipData(this.data[dataIndex]);
|
||||
|
||||
return `<div class='tooltip' style="background: ${this.pointBorderColor}">
|
||||
return `<div class='tooltip'>
|
||||
<p class='tooltip__value'>${dataPoint.date}<b class='tooltip__value__bold'> / ${dataPoint.value}</b></p>
|
||||
<div class='tooltip__arrow' style="background: ${this.pointBorderColor}" />
|
||||
<div class='tooltip__arrow' />
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@ -138,6 +91,7 @@ export default class DashboardChart extends BaseChart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #003dc1;
|
||||
|
||||
&__value {
|
||||
font-size: 14px;
|
||||
@ -157,6 +111,7 @@ export default class DashboardChart extends BaseChart {
|
||||
border-radius: 8px 0 0 0;
|
||||
transform: scale(1, 0.85) translate(0, 20%) rotate(45deg);
|
||||
margin-bottom: -4px;
|
||||
background-color: #003dc1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -58,7 +58,8 @@ export class ProjectsState {
|
||||
public totalLimits: ProjectLimits = new ProjectLimits();
|
||||
public cursor: ProjectsCursor = new ProjectsCursor();
|
||||
public page: ProjectsPage = new ProjectsPage();
|
||||
public bandwidthChartData: DataStamp[] = [];
|
||||
public allocatedBandwidthChartData: DataStamp[] = [];
|
||||
public settledBandwidthChartData: DataStamp[] = [];
|
||||
public storageChartData: DataStamp[] = [];
|
||||
public chartDataSince: Date = new Date();
|
||||
public chartDataBefore: Date = new Date();
|
||||
@ -181,7 +182,8 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState,
|
||||
state.currentLimits = new ProjectLimits();
|
||||
state.totalLimits = new ProjectLimits();
|
||||
state.storageChartData = [];
|
||||
state.bandwidthChartData = [];
|
||||
state.allocatedBandwidthChartData = [];
|
||||
state.settledBandwidthChartData = [];
|
||||
state.chartDataSince = new Date();
|
||||
state.chartDataBefore = new Date();
|
||||
},
|
||||
@ -193,7 +195,8 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState,
|
||||
state.page = page;
|
||||
},
|
||||
[SET_DAILY_DATA](state: ProjectsState, payload: ProjectsStorageBandwidthDaily) {
|
||||
state.bandwidthChartData = payload.bandwidth;
|
||||
state.allocatedBandwidthChartData = payload.allocatedBandwidth;
|
||||
state.settledBandwidthChartData = payload.settledBandwidth;
|
||||
state.storageChartData = payload.storage;
|
||||
},
|
||||
[SET_CHARTS_DATE_RANGE](state: ProjectsState, payload: ProjectUsageDateRange) {
|
||||
|
@ -1,6 +1,9 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { DataStamp } from "@/types/projects";
|
||||
import { Size } from "@/utils/bytesSize";
|
||||
|
||||
/**
|
||||
* ChartData class holds info for ChartData entity.
|
||||
*/
|
||||
@ -14,16 +17,16 @@ export class ChartData {
|
||||
borderColor: string,
|
||||
pointBorderColor: string,
|
||||
data: number[],
|
||||
secondaryBackgroundColor?: string,
|
||||
secondaryBorderColor?: string,
|
||||
secondaryPointBorderColor?: string,
|
||||
secondaryData?: number[],
|
||||
) {
|
||||
this.labels = labels;
|
||||
this.datasets[0] = new DataSets(backgroundColor, borderColor, pointBorderColor, data)
|
||||
|
||||
for (let i = 0; i < this.labels.length; i++) {
|
||||
this.datasets[i] = new DataSets(
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
pointBorderColor,
|
||||
data,
|
||||
);
|
||||
if (secondaryData && secondaryBackgroundColor && secondaryBorderColor && secondaryPointBorderColor) {
|
||||
this.datasets[1] = new DataSets(secondaryBackgroundColor, secondaryBorderColor, secondaryPointBorderColor, secondaryData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -223,7 +226,7 @@ export class Tooltip {
|
||||
Tooltip.elemStyling(tooltipStyling);
|
||||
}
|
||||
|
||||
private static createTooltip(id: string): HTMLElement {
|
||||
public static createTooltip(id: string): HTMLElement {
|
||||
let tooltipEl = document.getElementById(id);
|
||||
|
||||
if (!tooltipEl) {
|
||||
@ -235,15 +238,15 @@ export class Tooltip {
|
||||
return tooltipEl;
|
||||
}
|
||||
|
||||
private static remove(tooltipEl: HTMLElement) {
|
||||
public static remove(tooltipEl: HTMLElement): void {
|
||||
document.body.removeChild(tooltipEl);
|
||||
}
|
||||
|
||||
private static render(tooltip: HTMLElement, markUp: string) {
|
||||
private static render(tooltip: HTMLElement, markUp: string): void {
|
||||
tooltip.innerHTML = markUp;
|
||||
}
|
||||
|
||||
private static elemStyling(elemStyling: Styling) {
|
||||
private static elemStyling(elemStyling: Styling): void {
|
||||
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`;
|
||||
@ -251,6 +254,21 @@ export class Tooltip {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores data for chart's tooltip
|
||||
*/
|
||||
export class ChartTooltipData {
|
||||
public date: string;
|
||||
public value: string;
|
||||
|
||||
public constructor(stamp: DataStamp) {
|
||||
const size = new Size(stamp.value, 1)
|
||||
|
||||
this.date = stamp.intervalStart.toLocaleDateString('en-US', { day: '2-digit', month: 'short' });
|
||||
this.value = `${size.formattedBytes} ${size.label}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RenderChart contains definition for renderChart and addPlugin, that can be used to cast
|
||||
* a derived chart type, with `(this as unknown as RenderChart).renderChart`
|
||||
|
@ -211,8 +211,9 @@ export class DataStamp {
|
||||
*/
|
||||
export class ProjectsStorageBandwidthDaily {
|
||||
public constructor(
|
||||
public bandwidth: DataStamp[] = [],
|
||||
public storage: DataStamp[] = [],
|
||||
public allocatedBandwidth: DataStamp[] = [],
|
||||
public settledBandwidth: DataStamp[] = [],
|
||||
) {}
|
||||
}
|
||||
|
||||
|
@ -42,4 +42,28 @@ export class ChartUtils {
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to display correct number of days on chart's labels.
|
||||
*/
|
||||
public static daysDisplayedOnChart(from: Date, to: Date): string[] {
|
||||
const since = new Date(from);
|
||||
// Create an array of future displayed data points.
|
||||
const arr = Array<string>();
|
||||
|
||||
// If there is only one day chosen in date picker then we fill array with only one data point label.
|
||||
if (since.getTime() === to.getTime()) {
|
||||
arr.push(`${since.getMonth() + 1}/${since.getDate()}`);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Fill the data points array with correct data points labels.
|
||||
while (since <= to) {
|
||||
arr.push(`${since.getMonth() + 1}/${since.getDate()}`);
|
||||
since.setDate(since.getDate() + 1)
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
3
web/satellite/static/images/project/infoIcon.svg
Normal file
3
web/satellite/static/images/project/infoIcon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35ZM9.25 13.0745V10.429C9.25 9.97337 9.61937 9.604 10.075 9.604C10.5195 9.604 10.8819 9.95557 10.8993 10.3958L10.9 10.429V13.0196C10.9 13.4799 10.535 13.8572 10.075 13.8726C9.63427 13.8872 9.26511 13.5418 9.25044 13.1011C9.25015 13.0922 9.25 13.0834 9.25 13.0745ZM9.25 6.92454V6.829C9.25 6.37337 9.61937 6.004 10.075 6.004C10.5195 6.004 10.8819 6.35557 10.8993 6.79582L10.9 6.829V6.86964C10.9 7.32991 10.535 7.70724 10.075 7.72255C9.63427 7.73721 9.26511 7.39182 9.25044 6.9511C9.25015 6.94225 9.25 6.9334 9.25 6.92454Z" fill="#56606D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 916 B |
@ -4,7 +4,7 @@ exports[`BucketArea.vue renders correctly with bucket 1`] = `
|
||||
<div class="buckets-area">
|
||||
<div class="buckets-overflow">
|
||||
<div class="buckets-header">
|
||||
<p class="buckets-header__title">Buckets</p>
|
||||
<p class="buckets-header__title">Usage per bucket</p>
|
||||
<vheader-stub placeholder="Buckets" search="function () { [native code] }" class="buckets-header-component"></vheader-stub>
|
||||
</div>
|
||||
<div class="buckets-container">
|
||||
@ -23,7 +23,7 @@ exports[`BucketArea.vue renders correctly with pagination 1`] = `
|
||||
<div class="buckets-area">
|
||||
<div class="buckets-overflow">
|
||||
<div class="buckets-header">
|
||||
<p class="buckets-header__title">Buckets</p>
|
||||
<p class="buckets-header__title">Usage per bucket</p>
|
||||
<vheader-stub placeholder="Buckets" search="function () { [native code] }" class="buckets-header-component"></vheader-stub>
|
||||
</div>
|
||||
<div class="buckets-container">
|
||||
@ -51,7 +51,7 @@ exports[`BucketArea.vue renders correctly without search results 1`] = `
|
||||
<div class="buckets-area">
|
||||
<div class="buckets-overflow">
|
||||
<div class="buckets-header">
|
||||
<p class="buckets-header__title">Buckets</p>
|
||||
<p class="buckets-header__title">Usage per bucket</p>
|
||||
<vheader-stub placeholder="Buckets" search="function () { [native code] }" class="buckets-header-component"></vheader-stub>
|
||||
</div>
|
||||
<!---->
|
||||
|
Loading…
Reference in New Issue
Block a user