satellite/projectaccounting, satellite/console, web/satellite: implemented backend for bandwidth chart

Implemented endpoint and query to get bandwidth chart data for new project dashboard.
Connected backend with frontend.
Storage chart data is mocked right now.

Change-Id: Ib24d28614dc74bcc31b81ee3b8aa68b9898fa87b
This commit is contained in:
Vitalii Shpital 2021-12-07 16:41:39 +02:00
parent 5d6ee506b0
commit b8c55fdd87
16 changed files with 357 additions and 428 deletions

View File

@ -91,6 +91,18 @@ type ProjectLimits struct {
Segments *int64
}
// ProjectDailyUsage holds project daily usage.
type ProjectDailyUsage struct {
StorageUsage []ProjectUsageByDay `json:"storageUsage"`
BandwidthUsage []ProjectUsageByDay `json:"bandwidthUsage"`
}
// ProjectUsageByDay holds project daily usage.
type ProjectUsageByDay struct {
Date time.Time `json:"date"`
Value int64 `json:"value"`
}
// BucketUsage consist of total bucket usage for period.
type BucketUsage struct {
ProjectID uuid.UUID
@ -196,6 +208,10 @@ 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
// GetProjectDailyBandwidthByDateRange returns daily settled bandwidth usage for the specified date range.
GetProjectDailyBandwidthByDateRange(ctx context.Context, projectID uuid.UUID, from, to time.Time) ([]ProjectUsageByDay, error)
// GetProjectDailyStorageByDateRange returns daily storage usage for the specified date range.
GetProjectDailyStorageByDateRange(ctx context.Context, projectID uuid.UUID, from, to time.Time) ([]ProjectUsageByDay, error)
// UpdateProjectUsageLimit updates project usage limit.
UpdateProjectUsageLimit(ctx context.Context, projectID uuid.UUID, limit memory.Size) error

View File

@ -6,6 +6,8 @@ package consoleapi
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/zeebo/errs"
@ -101,6 +103,56 @@ func (ul *UsageLimits) TotalUsageLimits(w http.ResponseWriter, r *http.Request)
}
}
// DailyUsage returns daily usage by project ID.
func (ul *UsageLimits) DailyUsage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var ok bool
var idParam string
if idParam, ok = mux.Vars(r)["id"]; !ok {
ul.serveJSONError(w, http.StatusBadRequest, errs.New("missing project id route param"))
return
}
projectID, err := uuid.FromString(idParam)
if err != nil {
ul.serveJSONError(w, http.StatusBadRequest, errs.New("invalid project id: %v", err))
return
}
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64)
if err != nil {
ul.serveJSONError(w, http.StatusBadRequest, err)
return
}
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("to"), 10, 64)
if err != nil {
ul.serveJSONError(w, http.StatusBadRequest, err)
return
}
since := time.Unix(sinceStamp, 0).UTC()
before := time.Unix(beforeStamp, 0).UTC()
dailyUsage, err := ul.service.GetDailyProjectUsage(ctx, projectID, since, before)
if err != nil {
if console.ErrUnauthorized.Has(err) {
ul.serveJSONError(w, http.StatusUnauthorized, err)
return
}
ul.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = json.NewEncoder(w).Encode(dailyUsage)
if err != nil {
ul.log.Error("error encoding daily project usage", zap.Error(ErrUsageLimitsAPI.Wrap(err)))
}
}
// serveJSONError writes JSON error to response output stream.
func (ul *UsageLimits) serveJSONError(w http.ResponseWriter, status int, err error) {
serveJSONError(ul.log, w, status, err)

View File

@ -222,6 +222,10 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
"/api/v0/projects/usage-limits",
server.withAuth(http.HandlerFunc(usageLimitsController.TotalUsageLimits)),
).Methods(http.MethodGet)
router.Handle(
"/api/v0/projects/{id}/daily-usage",
server.withAuth(http.HandlerFunc(usageLimitsController.DailyUsage)),
).Methods(http.MethodGet)
authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, partners, server.analytics, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()

View File

@ -1662,6 +1662,36 @@ func (s *Service) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID
return result, nil
}
// GetDailyProjectUsage returns daily usage by project ID.
func (s *Service) GetDailyProjectUsage(ctx context.Context, projectID uuid.UUID, from, to time.Time) (_ *accounting.ProjectDailyUsage, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := s.getAuthAndAuditLog(ctx, "get daily usage by project ID")
if err != nil {
return nil, Error.Wrap(err)
}
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
if err != nil {
return nil, Error.Wrap(err)
}
bandwidthUsage, err := s.projectAccounting.GetProjectDailyBandwidthByDateRange(ctx, projectID, from, to)
if err != nil {
return nil, Error.Wrap(err)
}
storageUsage, err := s.projectAccounting.GetProjectDailyStorageByDateRange(ctx, projectID, from, to)
if err != nil {
return nil, Error.Wrap(err)
}
return &accounting.ProjectDailyUsage{
StorageUsage: storageUsage,
BandwidthUsage: bandwidthUsage,
}, nil
}
// GetProjectUsageLimits returns project limits and current usage.
//
// Among others,it can return one of the following errors returned by

View File

@ -199,6 +199,46 @@ func (db *ProjectAccounting) GetProjectDailyBandwidth(ctx context.Context, proje
return allocated, settled, dead, err
}
// GetProjectDailyBandwidthByDateRange returns project daily settled bandwidth usage by specific date range.
func (db *ProjectAccounting) GetProjectDailyBandwidthByDateRange(ctx context.Context, projectID uuid.UUID, from, to time.Time) (_ []accounting.ProjectUsageByDay, err error) {
defer mon.Task()(&ctx)(&err)
usage := make([]accounting.ProjectUsageByDay, 0)
query := db.db.Rebind(`SELECT interval_day, COALESCE(egress_allocated, 0) FROM project_bandwidth_daily_rollups WHERE project_id = ? AND (interval_day BETWEEN ? AND ?)`)
rows, err := db.db.QueryContext(ctx, query, projectID[:], from, to)
if err != nil {
return nil, err
}
for rows.Next() {
var day time.Time
var amount int64
err = rows.Scan(&day, &amount)
if err != nil {
return nil, errs.Combine(err, rows.Close())
}
usage = append(usage, accounting.ProjectUsageByDay{
Date: day,
Value: amount,
})
}
err = errs.Combine(rows.Err(), rows.Close())
return usage, err
}
// GetProjectDailyStorageByDateRange returns project daily storage usage by specific date range.
func (db *ProjectAccounting) GetProjectDailyStorageByDateRange(ctx context.Context, _ uuid.UUID, _, _ time.Time) (_ []accounting.ProjectUsageByDay, err error) {
defer mon.Task()(&ctx)(&err)
usage := make([]accounting.ProjectUsageByDay, 0)
return usage, err
}
// DeleteProjectBandwidthBefore deletes project bandwidth rollups before the given time.
func (db *ProjectAccounting) DeleteProjectBandwidthBefore(ctx context.Context, before time.Time) (err error) {
defer mon.Task()(&ctx)(&err)

View File

@ -4,14 +4,17 @@
import { BaseGql } from '@/api/baseGql';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import {
DataStamp,
Project,
ProjectFields,
ProjectLimits,
ProjectsApi,
ProjectsCursor,
ProjectsPage,
ProjectsStorageBandwidthDaily,
} from '@/types/projects';
import { HttpClient } from '@/utils/httpClient';
import { Time } from "@/utils/time";
export class ProjectsApiGql extends BaseGql implements ProjectsApi {
private readonly http: HttpClient = new HttpClient();
@ -188,6 +191,51 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
throw new Error('can not get total usage limits');
}
/**
* Get project daily usage for specific date range.
*
* @param projectId- project ID
* @param start- since date
* @param end- before date
* 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}`;
const response = await this.http.get(path);
if (response.ok) {
const usage = await response.json();
return new ProjectsStorageBandwidthDaily(
usage.bandwidthUsage.map(el => {
// Set the timestamps to be the beginning of the day.
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.
const date = new Date(el.date)
date.setHours(0, 0, 0, 0)
return new DataStamp(el.value, date)
}),
);
}
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not get project daily usage');
}
/**
* Fetch owned projects.
*

View File

@ -3,7 +3,7 @@
<script lang="ts">
import { Line } from 'vue-chartjs';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { ChartData, RenderChart } from '@/types/chart';
@ -68,6 +68,14 @@ export default class VChart extends Vue {
(this as unknown as RenderChart).renderChart(this.chartData, this.chartOptions);
}
@Watch('chartData')
private onDataChange(_news: Record<string, unknown>, _old: Record<string, unknown>) {
/**
* renderChart method is inherited from BaseChart which is extended by VChart.Line
*/
(this as unknown as RenderChart).renderChart(this.chartData, this.chartOptions);
}
/**
* Returns chart options.
*/
@ -77,6 +85,11 @@ export default class VChart extends Vue {
return {
responsive: false,
maintainAspectRatios: false,
animation: false,
hover: {
animationDuration: 0
},
responsiveAnimationDuration: 0,
legend: {
display: false,
},
@ -112,14 +125,9 @@ export default class VChart extends Vue {
},
tooltips: {
enabled: false,
custom: (tooltipModel) => {
this.tooltipConstructor(tooltipModel);
},
labels: {
enabled: true,
},
},
};
}

View File

@ -19,7 +19,6 @@ 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";
@ -43,7 +42,7 @@ class ChartTooltip {
components: { VChart }
})
export default class DashboardChart extends BaseChart {
@Prop({default: []})
@Prop({default: () => []})
public readonly data: DataStamp[];
@Prop({default: 'chart'})
public readonly name: string;
@ -53,13 +52,17 @@ export default class DashboardChart extends BaseChart {
public readonly borderColor: string;
@Prop({default: ''})
public readonly pointBorderColor: string;
@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 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);
const data: number[] = this.data.map(el => el.value)
const xAxisDateLabels: string[] = this.daysDisplayedOnChart();
return new ChartData(
xAxisDateLabels,
@ -80,6 +83,30 @@ export default class DashboardChart extends BaseChart {
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.
*/

View File

@ -11,8 +11,8 @@
<DatepickerIcon class="range-selection__toggle-container__icon" />
<h1 class="range-selection__toggle-container__label">{{ dateRangeLabel }}</h1>
</div>
<div v-if="isOpen" v-click-outside="closePicker" class="range-selection__popup">
<VDateRangePicker :on-date-pick="onDatePick" :is-open="true" :date-range="defaultDateRange" />
<div v-show="isOpen" v-click-outside="closePicker" class="range-selection__popup">
<VDateRangePicker :on-date-pick="onDatePick" :is-open="true" :date-range="pickerDateRange" />
</div>
</div>
</template>
@ -21,7 +21,6 @@
import { Component, Prop, Vue } from 'vue-property-decorator';
import { APP_STATE_ACTIONS } from "@/utils/constants/actionNames";
import { ProjectUsageDateRange } from "@/types/projects";
import VDateRangePicker from "@/components/common/VDateRangePicker.vue";
@ -36,8 +35,10 @@ import DatepickerIcon from '@/../static/images/project/datepicker.svg';
})
export default class DateRangeSelection extends Vue {
@Prop({ default: null })
public readonly dateRange: ProjectUsageDateRange | null;
@Prop({ default: new Date() })
public readonly since: Date;
@Prop({ default: new Date() })
public readonly before: Date;
@Prop({ default: () => false })
public readonly onDatePick: (dateRange: Date[]) => void;
@Prop({ default: () => false })
@ -56,30 +57,20 @@ export default class DateRangeSelection extends Vue {
* Returns formatted date range string.
*/
public get dateRangeLabel(): string {
if (!this.dateRange) {
return 'Last 30 days';
if (this.since.getTime() === this.before.getTime()) {
return this.since.toLocaleDateString('en-US')
}
if (this.dateRange.since.getTime() === this.dateRange.before.getTime()) {
return this.dateRange.since.toLocaleDateString('en-US')
}
const sinceFormattedString = this.dateRange.since.toLocaleDateString('en-US');
const beforeFormattedString = this.dateRange.before.toLocaleDateString('en-US');
const sinceFormattedString = this.since.toLocaleDateString('en-US');
const beforeFormattedString = this.before.toLocaleDateString('en-US');
return `${sinceFormattedString}-${beforeFormattedString}`;
}
/**
* Returns default date range.
* Returns date range to be displayed in date range picker.
*/
public get defaultDateRange(): Date[] {
if (this.dateRange) {
return [this.dateRange.since, this.dateRange.before]
}
const previous = new Date()
previous.setMonth(previous.getMonth() - 1)
return [previous, new Date()]
public get pickerDateRange(): Date[] {
return [this.since, this.before]
}
}
</script>

View File

@ -28,7 +28,8 @@
<h2 class="project-dashboard__stats-header__title">Project Stats</h2>
<div class="project-dashboard__stats-header__buttons">
<DateRangeSelection
:date-range="chartsDateRange"
:since="chartsSinceDate"
:before="chartsBeforeDate"
:on-date-pick="onChartsDateRangePick"
:is-open="isChartsDatePicker"
:toggle="toggleChartsDatePicker"
@ -70,6 +71,8 @@
:width="chartWidth"
:height="170"
:data="storageUsage"
:since="chartsSinceDate"
:before="chartsBeforeDate"
background-color="#E6EDF7"
border-color="#D7E8FF"
point-border-color="#003DC1"
@ -88,6 +91,8 @@
:width="chartWidth"
:height="170"
:data="bandwidthUsage"
:since="chartsSinceDate"
:before="chartsBeforeDate"
background-color="#FFE0E7"
border-color="#FFC0CF"
point-border-color="#FF458B"
@ -141,12 +146,13 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { PROJECTS_ACTIONS, PROJECTS_MUTATIONS } from "@/store/modules/projects";
import { PROJECTS_ACTIONS } from "@/store/modules/projects";
import { PAYMENTS_ACTIONS, PAYMENTS_MUTATIONS } from "@/store/modules/payments";
import { APP_STATE_ACTIONS } from "@/utils/constants/actionNames";
import { RouteConfig } from "@/router";
import { DataStamp, ProjectLimits, ProjectsStorageBandwidthDaily, ProjectUsageDateRange } from "@/types/projects";
import { DataStamp, ProjectLimits } from "@/types/projects";
import { Dimensions, Size } from "@/utils/bytesSize";
import { ChartUtils } from "@/utils/chart";
import VLoader from "@/components/common/VLoader.vue";
import InfoContainer from "@/components/project/newProjectDashboard/InfoContainer.vue";
@ -192,7 +198,11 @@ export default class NewProjectDashboard extends Vue {
this.recalculateChartWidth();
try {
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, new ProjectsStorageBandwidthDaily(this.chartTestData(), this.chartTestData()));
const now = new Date()
const past = new Date()
past.setDate(past.getDate() - 30)
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, {since: past, before: now});
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);
@ -255,11 +265,15 @@ export default class NewProjectDashboard extends Vue {
/**
* onChartsDateRangePick holds logic for choosing date range for charts.
* Fetches new data for specific date range.
* @param dateRange
*/
public onChartsDateRangePick(dateRange: Date[]): void {
// TODO: rework when backend is ready
this.$store.commit(PROJECTS_MUTATIONS.SET_CHARTS_DATE_RANGE, new ProjectUsageDateRange(dateRange[0], dateRange[1]));
public async onChartsDateRangePick(dateRange: Date[]): Promise<void> {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, {since: dateRange[0], before: dateRange[1]})
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
@ -308,14 +322,28 @@ export default class NewProjectDashboard extends Vue {
* Returns storage chart data from store.
*/
public get storageUsage(): DataStamp[] {
return this.$store.state.projectsModule.storageChartData;
return ChartUtils.populateEmptyUsage(this.$store.state.projectsModule.storageChartData, this.chartsSinceDate, this.chartsBeforeDate);
}
/**
* Returns bandwidth chart data from store.
*/
public get bandwidthUsage(): DataStamp[] {
return this.$store.state.projectsModule.bandwidthChartData;
return ChartUtils.populateEmptyUsage(this.$store.state.projectsModule.bandwidthChartData, this.chartsSinceDate, this.chartsBeforeDate);
}
/**
* Returns charts since date from store.
*/
public get chartsSinceDate(): Date {
return this.$store.state.projectsModule.chartDataSince;
}
/**
* Returns charts before date from store.
*/
public get chartsBeforeDate(): Date {
return this.$store.state.projectsModule.chartDataBefore;
}
/**
@ -329,36 +357,12 @@ export default class NewProjectDashboard extends Vue {
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);
})
}
/**
* Returns charts date range from store.
*/
private get chartsDateRange(): ProjectUsageDateRange | null {
return this.$store.getters.chartsDateRange;
}
}
</script>
<style scoped lang="scss">
.project-dashboard {
padding: 56px 40px;
padding: 56px 55px 56px 40px;
height: calc(100% - 112px);
max-width: calc(100vw - 280px - 80px);
background-image: url('../../../../static/images/project/background.png');

View File

@ -60,8 +60,8 @@ export class ProjectsState {
public page: ProjectsPage = new ProjectsPage();
public bandwidthChartData: DataStamp[] = [];
public storageChartData: DataStamp[] = [];
public chartDataSince: Date | null = null;
public chartDataBefore: Date | null = null;
public chartDataSince: Date = new Date();
public chartDataBefore: Date = new Date();
}
interface ProjectsContext {
@ -182,8 +182,8 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState,
state.totalLimits = new ProjectLimits();
state.storageChartData = [];
state.bandwidthChartData = [];
state.chartDataSince = null;
state.chartDataBefore = null;
state.chartDataSince = new Date();
state.chartDataBefore = new Date();
},
[SET_PAGE_NUMBER](state: ProjectsState, pageNumber: number) {
state.cursor.page = pageNumber;
@ -217,9 +217,11 @@ 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);
[FETCH_DAILY_DATA]: async function ({commit, state}: ProjectsContext, payload: ProjectUsageDateRange): Promise<void> {
const usage: ProjectsStorageBandwidthDaily = await api.getDailyUsage(state.selectedProject.id, payload.since, payload.before)
commit(SET_CHARTS_DATE_RANGE, payload)
commit(SET_DAILY_DATA, usage);
},
[CREATE]: async function ({commit}: ProjectsContext, createProjectFields: ProjectFields): Promise<Project> {
const project = await api.create(createProjectFields);
@ -357,13 +359,6 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState,
return projectsCount;
},
chartsDateRange: (state: ProjectsState): ProjectUsageDateRange | null => {
if (!state.chartDataSince || !state.chartDataBefore) {
return null
}
return new ProjectUsageDateRange(state.chartDataSince, state.chartDataBefore);
},
},
};
}

View File

@ -52,6 +52,13 @@ export interface ProjectsApi {
*/
getTotalLimits(): Promise<ProjectLimits>;
/**
* Get project daily usage by specific date range.
*
* throws Error
*/
getDailyUsage(projectID: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily>;
/**
* Fetch owned projects.
*
@ -184,25 +191,18 @@ export class ProjectsCursor {
* 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;
}
public constructor(
public value = 0,
public intervalStart = new Date()
) {}
/**
* Creates new empty instance of stamp with defined date
* @param date - holds specific date of the month
* @param date - holds specific date of the date range
* @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);
public static emptyWithDate(date: Date): DataStamp {
return new DataStamp(0, date);
}
}
@ -219,9 +219,7 @@ export class ProjectsStorageBandwidthDaily {
/**
* ProjectUsageDateRange is used to describe project's usage by date range.
*/
export class ProjectUsageDateRange {
public constructor(
public since: Date,
public before: Date,
) {}
export interface ProjectUsageDateRange {
since: Date;
before: Date;
}

View File

@ -1,27 +1,45 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import { DataStamp } from "@/types/projects";
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
* Adds missing usage for chart data for each day of date range.
* @param fetchedData - array of data that is spread over missing usage for each day of the date range
* @param since - instance of since date
* @param before - instance of before date
* @returns chartData - array of filled data
*/
public static daysDisplayedOnChart(start: Date, end: Date): string[] {
const arr = Array<string>();
public static populateEmptyUsage(fetchedData: DataStamp[], since: Date, before: Date): DataStamp[] {
// Create an array of day-by-day dates that will be displayed on chart according to given date range.
const datesArr = new Array<Date>();
const dt = new Date(since);
dt.setHours(0, 0, 0, 0);
if (start === end) {
arr.push(`${start.getMonth() + 1}/${start.getDate()}`);
return arr;
// Fill previously created array with day-by-day dates.
while (dt.getTime() <= before.getTime()) {
datesArr.push(new Date(dt));
dt.setDate(dt.getDate() + 1);
}
const dt = start;
while (dt <= end) {
arr.push(`${dt.getMonth() + 1}/${dt.getDate()}`);
dt.setDate(dt.getDate() + 1)
// Create new array of objects with date and corresponding data value with length of date range difference.
const chartData: DataStamp[] = new Array(datesArr.length);
// Fill new array.
for (let i = 0; i < datesArr.length; i++) {
// Find in fetched data a day-data value that corresponds to current iterable date.
const foundData = fetchedData.find(el => el.intervalStart.getTime() === datesArr[i].getTime())
// If found then fill new array with appropriate day-data value.
if (foundData) {
chartData[i] = foundData;
continue;
}
// If not found then fill new array with day and zero data value.
chartData[i] = DataStamp.emptyWithDate(datesArr[i]);
}
return arr;
return chartData;
}
}

View File

@ -1,219 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* Options is a set of options used for VDatePicker.vue.
*/
export class Options {
public constructor(
public mondayFirstWeek: string[] = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
public sundayFirstWeek: string[] = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
public month: string[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
public color = {
checked: '#2683FF',
header: '#2683FF',
headerText: '#444C63',
},
public inputStyle = {
'visibility': 'hidden',
'width': '0',
},
public overlayOpacity: number = 0.5,
public dismissible: boolean = true,
) {}
}
/**
* DayItem is used to store information about day cell in calendar.
*/
export class DayItem {
public constructor(
public value: number,
public inMonth: boolean,
public unavailable: boolean,
public checked: boolean,
public moment: Date,
public action: DayAction = DayAction.Default,
public today: boolean = false,
) {}
public equals(dateToCompare: Date): boolean {
const isDayEqual = this.moment.getDate() === dateToCompare.getDate();
const isMonthEqual = this.moment.getMonth() === dateToCompare.getMonth();
const isYearEqual = this.moment.getFullYear() === dateToCompare.getFullYear();
return isDayEqual && isMonthEqual && isYearEqual;
}
}
/**
* DayAction is enum represents month change direction on day click.
*/
export enum DayAction {
Next,
Previous,
Default,
}
/**
* DateStamp is cozy representation of Date for view.
*/
export class DateStamp {
public constructor(
public year: number,
public month: number,
public day: number,
) {}
public fromDate(date: Date): void {
this.year = date.getFullYear();
this.month = date.getMonth();
this.day = date.getDate();
}
}
/**
* DisplayedType is enum represents view type to show in calendar to check.
*/
export enum DisplayedType {
Day,
Month,
Year,
}
/**
* DateGenerator is utility class used for generating DayItem and year lists for calendar.
*/
export class DateGenerator {
private current: DateStamp;
private isSundayFirst: boolean;
private now = new Date();
public populateDays(current: DateStamp, isSundayFirst: boolean): DayItem[] {
this.current = current;
this.isSundayFirst = isSundayFirst;
const days: DayItem[] = [];
this.populateSelectedMonthDays(days);
this.populatePreviousMonthDays(days);
this.populateNextMonthDays(days);
this.markToday(days);
return days;
}
public populateYears(): number[] {
const year = new Date().getFullYear();
const years: number[] = [];
for (let i = year - 99; i <= year; i++) {
years.unshift(i);
}
return years;
}
private populateSelectedMonthDays(days: DayItem[]): void {
const daysInSelectedMonth = new Date(this.current.year, this.current.month + 1, 0).getDate();
const currentMonth = this.now.getMonth();
for (let i = 1; i <= daysInSelectedMonth; i++) {
const moment = new Date(this.current.year, this.current.month, this.current.day, 23, 59);
moment.setDate(i);
days.push(
new DayItem(
i,
this.current.month !== currentMonth || (this.current.month === currentMonth && i <= this.now.getDate()),
false,
false,
moment,
),
);
}
}
private populatePreviousMonthDays(days: DayItem[]): void {
const previousMonth = new Date(this.current.year, this.current.month, this.current.day);
previousMonth.setMonth(previousMonth.getMonth() - 1);
const firstDate = new Date(this.current.year, this.current.month, this.current.day);
firstDate.setDate(1);
let firstDay = firstDate.getDay();
if (firstDay === 0) firstDay = 7;
const daysInPreviousMonth = new Date(previousMonth.getFullYear(), previousMonth.getMonth() + 1, 0).getDate();
for (let i = 0; i < firstDay - (this.isSundayFirst ? 0 : 1); i++) {
const moment = new Date(this.current.year, this.current.month, this.current.day, 23, 59);
moment.setDate(1);
moment.setMonth(moment.getMonth() - 1);
moment.setDate(new Date(moment.getFullYear(), moment.getMonth() + 1, 0).getDate() - i);
days.unshift(
new DayItem(
daysInPreviousMonth - i,
false,
false,
false,
moment,
DayAction.Previous,
),
);
}
}
private populateNextMonthDays(days: DayItem[]): void {
const passiveDaysAtFinal = 42 - days.length;
for (let i = 1; i <= passiveDaysAtFinal; i++) {
const moment = new Date(this.current.year, this.current.month, this.current.day, 23, 59);
moment.setMonth(moment.getMonth() + 1);
moment.setDate(i);
days.push(
new DayItem(
i,
false,
false,
false,
moment,
DayAction.Next,
),
);
}
}
private markToday(days: DayItem[]): void {
const daysCount = days.length;
for (let i = 0; i < daysCount; i++) {
const day: DayItem = days[i];
if (day.equals(this.now)) {
day.today = true;
break;
}
}
}
}
/**
* DateFormat is utils class for date formatting to string.
*/
export class DateFormat {
/**
* getUSDate transforms date into US date format string.
* @param date - Date to format
* @param separator - symbol for joining date string
* @returns formatted date string
*/
public static getUTCDate(date: Date, separator: string): string {
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const year = date.getUTCFullYear();
return [month, day, year].join(separator);
}
}

View File

@ -1,7 +1,15 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { Project, ProjectFields, ProjectLimits, ProjectsApi, ProjectsCursor, ProjectsPage } from '@/types/projects';
import {
Project,
ProjectFields,
ProjectLimits,
ProjectsApi,
ProjectsCursor,
ProjectsPage,
ProjectsStorageBandwidthDaily
} from '@/types/projects';
/**
* Mock for ProjectsApi
@ -46,4 +54,8 @@ export class ProjectsApiMock implements ProjectsApi {
getTotalLimits(): Promise<ProjectLimits> {
return Promise.resolve(this.mockLimits);
}
getDailyUsage(_projectId: string, _start: Date, _end: Date): Promise<ProjectsStorageBandwidthDaily> {
throw new Error('not implemented');
}
}

View File

@ -1,95 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import {
DateFormat,
DateGenerator,
DateStamp,
DayItem,
} from '@/utils/datepicker';
describe('datepicker', () => {
it('DateGenerator populate years correctly', () => {
const dateGenerator = new DateGenerator();
const currentYear = new Date().getFullYear();
const years = dateGenerator.populateYears();
expect(years.length).toBe(100);
expect(years[0]).toBe(currentYear);
});
it('DateGenerator populate days correctly with exact date and isSundayFirst', () => {
const dateGenerator = new DateGenerator();
// 8th month is september
const currentDate = new DateStamp(2019, 8, 30);
const firstExpectedDay = new DayItem(
25,
false,
false,
false,
new Date(2019, 7, 25),
1,
false,
);
const lastExpectedDay = new DayItem(
5,
false,
false,
false,
new Date(2019, 9, 5),
1,
false,
);
const days = dateGenerator.populateDays(currentDate, true);
expect(days.length).toBe(42);
expect(days[0].equals(firstExpectedDay.moment)).toBe(true);
expect(days[days.length - 1].equals(lastExpectedDay.moment)).toBe(true);
});
it('DateGenerator populate days correctly with exact date and no isSundayFirst', () => {
const dateGenerator = new DateGenerator();
// 8th month is september
const currentDate = new DateStamp(2019, 8, 30);
const firstExpectedDay = new DayItem(
26,
false,
false,
false,
new Date(2019, 7, 26),
1,
false,
);
const lastExpectedDay = new DayItem(
6,
false,
false,
false,
new Date(2019, 9, 6),
1,
false,
);
const days = dateGenerator.populateDays(currentDate, false);
expect(days.length).toBe(42);
expect(days[0].equals(firstExpectedDay.moment)).toBe(true);
expect(days[days.length - 1].equals(lastExpectedDay.moment)).toBe(true);
});
it('DateFormat formats date to string correctly', () => {
const testDate1 = new Date(Date.UTC(2019, 10, 7));
const testDate2 = new Date(Date.UTC(2019, 1, 1));
const expectedResult1 = '11/7/2019';
const expectedResult2 = '2-1-2019';
const actualResult1 = DateFormat.getUTCDate(testDate1, '/');
const actualResult2 = DateFormat.getUTCDate(testDate2, '-');
expect(actualResult1).toBe(expectedResult1);
expect(actualResult2).toBe(expectedResult2);
});
});