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:
parent
5d6ee506b0
commit
b8c55fdd87
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user