From b8c55fdd877d056b4a387fab723f8722f3dfd208 Mon Sep 17 00:00:00 2001 From: Vitalii Shpital Date: Tue, 7 Dec 2021 16:41:39 +0200 Subject: [PATCH] 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 --- satellite/accounting/db.go | 16 ++ .../consoleweb/consoleapi/usagelimits.go | 52 +++++ satellite/console/consoleweb/server.go | 4 + satellite/console/service.go | 30 +++ satellite/satellitedb/projectaccounting.go | 40 ++++ web/satellite/src/api/projects.ts | 48 ++++ .../src/components/common/VChart.vue | 20 +- .../newProjectDashboard/DashboardChart.vue | 35 ++- .../DateRangeSelection.vue | 35 ++- .../NewProjectDashboard.vue | 72 +++--- web/satellite/src/store/modules/projects.ts | 23 +- web/satellite/src/types/projects.ts | 36 ++- web/satellite/src/utils/chart.ts | 46 ++-- web/satellite/src/utils/datepicker.ts | 219 ------------------ web/satellite/tests/unit/mock/api/projects.ts | 14 +- .../tests/unit/utils/datepicker.spec.ts | 95 -------- 16 files changed, 357 insertions(+), 428 deletions(-) delete mode 100644 web/satellite/src/utils/datepicker.ts delete mode 100644 web/satellite/tests/unit/utils/datepicker.spec.ts diff --git a/satellite/accounting/db.go b/satellite/accounting/db.go index 1e3fb6a15..53da7d28f 100644 --- a/satellite/accounting/db.go +++ b/satellite/accounting/db.go @@ -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 diff --git a/satellite/console/consoleweb/consoleapi/usagelimits.go b/satellite/console/consoleweb/consoleapi/usagelimits.go index 678fc8816..3691b1659 100644 --- a/satellite/console/consoleweb/consoleapi/usagelimits.go +++ b/satellite/console/consoleweb/consoleapi/usagelimits.go @@ -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) diff --git a/satellite/console/consoleweb/server.go b/satellite/console/consoleweb/server.go index 67d16afde..6a20315aa 100644 --- a/satellite/console/consoleweb/server.go +++ b/satellite/console/consoleweb/server.go @@ -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() diff --git a/satellite/console/service.go b/satellite/console/service.go index 5f7af8d8b..76b17c054 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -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 diff --git a/satellite/satellitedb/projectaccounting.go b/satellite/satellitedb/projectaccounting.go index 784c6effe..d04d77340 100644 --- a/satellite/satellitedb/projectaccounting.go +++ b/satellite/satellitedb/projectaccounting.go @@ -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) diff --git a/web/satellite/src/api/projects.ts b/web/satellite/src/api/projects.ts index 6120e0144..94ac0cc0a 100644 --- a/web/satellite/src/api/projects.ts +++ b/web/satellite/src/api/projects.ts @@ -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 { + // 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. * diff --git a/web/satellite/src/components/common/VChart.vue b/web/satellite/src/components/common/VChart.vue index eba208764..de021259d 100644 --- a/web/satellite/src/components/common/VChart.vue +++ b/web/satellite/src/components/common/VChart.vue @@ -3,7 +3,7 @@ diff --git a/web/satellite/src/components/project/newProjectDashboard/NewProjectDashboard.vue b/web/satellite/src/components/project/newProjectDashboard/NewProjectDashboard.vue index a84f11652..50b59a21c 100644 --- a/web/satellite/src/components/project/newProjectDashboard/NewProjectDashboard.vue +++ b/web/satellite/src/components/project/newProjectDashboard/NewProjectDashboard.vue @@ -28,7 +28,8 @@

Project Stats

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 { + 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(); - 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; - } }