satellite/{console, web}: added detailed usage report
Allow user to download detailed usage report from Billing -> Overview screen. Report is a CSV file containing usage data for all the projects user owns. Issue: https://github.com/storj/storj/issues/6154 Change-Id: I3109002bf37b1313652a2be3447aaa7bc6204887
This commit is contained in:
parent
41e16bc398
commit
e3713fddb8
@ -5,6 +5,7 @@ package accounting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"storj.io/common/memory"
|
"storj.io/common/memory"
|
||||||
@ -164,6 +165,23 @@ type BucketUsageRollup struct {
|
|||||||
Before time.Time `json:"before"`
|
Before time.Time `json:"before"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToStringSlice converts rollup values to a slice of strings.
|
||||||
|
func (b *BucketUsageRollup) ToStringSlice() []string {
|
||||||
|
return []string{
|
||||||
|
b.ProjectID.String(),
|
||||||
|
b.BucketName,
|
||||||
|
fmt.Sprintf("%f", b.TotalStoredData),
|
||||||
|
fmt.Sprintf("%f", b.TotalSegments),
|
||||||
|
fmt.Sprintf("%f", b.ObjectCount),
|
||||||
|
fmt.Sprintf("%f", b.MetadataSize),
|
||||||
|
fmt.Sprintf("%f", b.RepairEgress),
|
||||||
|
fmt.Sprintf("%f", b.GetEgress),
|
||||||
|
fmt.Sprintf("%f", b.AuditEgress),
|
||||||
|
b.Since.String(),
|
||||||
|
b.Before.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Usage contains project's usage split on segments and storage.
|
// Usage contains project's usage split on segments and storage.
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
Storage int64
|
Storage int64
|
||||||
|
@ -5,6 +5,7 @@ package consoleapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -105,6 +106,63 @@ func (ul *UsageLimits) TotalUsageLimits(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TotalUsageReport returns total usage report for all the projects that user owns.
|
||||||
|
func (ul *UsageLimits) TotalUsageReport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
var err error
|
||||||
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
|
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ul.serveJSONError(ctx, w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("before"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ul.serveJSONError(ctx, w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
since := time.Unix(sinceStamp, 0).UTC()
|
||||||
|
before := time.Unix(beforeStamp, 0).UTC()
|
||||||
|
|
||||||
|
usage, err := ul.service.GetTotalUsageReport(ctx, since, before)
|
||||||
|
if err != nil {
|
||||||
|
if console.ErrUnauthorized.Has(err) {
|
||||||
|
ul.serveJSONError(ctx, w, http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.serveJSONError(ctx, w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := "storj-report-" + since.Format("2006-01-02") + "-to-" + before.Format("2006-01-02") + ".csv"
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/csv")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment;filename="+fileName)
|
||||||
|
|
||||||
|
wr := csv.NewWriter(w)
|
||||||
|
|
||||||
|
csvHeaders := []string{"ProjectID", "BucketName", "TotalStoredData GB-hour", "TotalSegments GB-hour", "ObjectCount GB-hour", "MetadataSize GB-hour", "RepairEgress GB", "GetEgress GB", "AuditEgress GB", "Since", "Before"}
|
||||||
|
|
||||||
|
err = wr.Write(csvHeaders)
|
||||||
|
if err != nil {
|
||||||
|
ul.serveJSONError(ctx, w, http.StatusInternalServerError, errs.New("Error writing CSV data"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range usage {
|
||||||
|
err = wr.Write(u.ToStringSlice())
|
||||||
|
if err != nil {
|
||||||
|
ul.serveJSONError(ctx, w, http.StatusInternalServerError, errs.New("Error writing CSV data"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wr.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
// DailyUsage returns daily usage by project ID.
|
// DailyUsage returns daily usage by project ID.
|
||||||
func (ul *UsageLimits) DailyUsage(w http.ResponseWriter, r *http.Request) {
|
func (ul *UsageLimits) DailyUsage(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
package consoleapi_test
|
package consoleapi_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ import (
|
|||||||
"storj.io/storj/satellite"
|
"storj.io/storj/satellite"
|
||||||
"storj.io/storj/satellite/accounting"
|
"storj.io/storj/satellite/accounting"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
|
"storj.io/storj/satellite/metabase"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_TotalUsageLimits(t *testing.T) {
|
func Test_TotalUsageLimits(t *testing.T) {
|
||||||
@ -167,3 +170,94 @@ func Test_DailyUsage(t *testing.T) {
|
|||||||
require.GreaterOrEqual(t, output.SettledBandwidthUsage[0].Value, 5*memory.KiB)
|
require.GreaterOrEqual(t, output.SettledBandwidthUsage[0].Value, 5*memory.KiB)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_TotalUsageReport(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) {
|
||||||
|
var (
|
||||||
|
satelliteSys = planet.Satellites[0]
|
||||||
|
uplink = planet.Uplinks[0]
|
||||||
|
now = time.Now()
|
||||||
|
inFiveMinutes = now.Add(5 * time.Minute)
|
||||||
|
inAnHour = now.Add(1 * time.Hour)
|
||||||
|
since = fmt.Sprintf("%d", now.Unix())
|
||||||
|
before = fmt.Sprintf("%d", inAnHour.Unix())
|
||||||
|
expectedCSVValue = fmt.Sprintf("%f", float64(0))
|
||||||
|
)
|
||||||
|
|
||||||
|
newUser := console.CreateUser{
|
||||||
|
FullName: "Total Usage Report Test",
|
||||||
|
ShortName: "",
|
||||||
|
Email: "ur@test.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := satelliteSys.AddUser(ctx, newUser, 3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project1, err := satelliteSys.AddProject(ctx, user.ID, "testProject1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project2, err := satelliteSys.AddProject(ctx, user.ID, "testProject2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bucketName := "bucket"
|
||||||
|
err = uplink.CreateBucket(ctx, satelliteSys, bucketName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bucketLoc1 := metabase.BucketLocation{
|
||||||
|
ProjectID: project1.ID,
|
||||||
|
BucketName: bucketName,
|
||||||
|
}
|
||||||
|
tally1 := &accounting.BucketTally{
|
||||||
|
BucketLocation: bucketLoc1,
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketLoc2 := metabase.BucketLocation{
|
||||||
|
ProjectID: project2.ID,
|
||||||
|
BucketName: bucketName,
|
||||||
|
}
|
||||||
|
tally2 := &accounting.BucketTally{
|
||||||
|
BucketLocation: bucketLoc2,
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketTallies := map[metabase.BucketLocation]*accounting.BucketTally{
|
||||||
|
bucketLoc1: tally1,
|
||||||
|
bucketLoc2: tally2,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = satelliteSys.DB.ProjectAccounting().SaveTallies(ctx, inFiveMinutes, bucketTallies)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
endpoint := fmt.Sprintf("projects/total-usage-report?since=%s&before=%s", since, before)
|
||||||
|
body, status, err := doRequestWithAuth(ctx, t, satelliteSys, user, http.MethodGet, endpoint, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, status)
|
||||||
|
|
||||||
|
content := string(body)
|
||||||
|
reader := csv.NewReader(strings.NewReader(content))
|
||||||
|
records, err := reader.ReadAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, records, 3)
|
||||||
|
|
||||||
|
expectedHeaders := []string{"ProjectID", "BucketName", "TotalStoredData GB-hour", "TotalSegments GB-hour", "ObjectCount GB-hour", "MetadataSize GB-hour", "RepairEgress GB", "GetEgress GB", "AuditEgress GB", "Since", "Before"}
|
||||||
|
for i, header := range expectedHeaders {
|
||||||
|
require.Equal(t, header, records[0][i])
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, project1.PublicID.String(), records[1][0])
|
||||||
|
require.Equal(t, project2.PublicID.String(), records[2][0])
|
||||||
|
require.Equal(t, bucketName, records[1][1])
|
||||||
|
require.Equal(t, bucketName, records[2][1])
|
||||||
|
for i := 2; i < 9; i++ {
|
||||||
|
require.Equal(t, expectedCSVValue, records[1][i])
|
||||||
|
require.Equal(t, expectedCSVValue, records[2][i])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -296,6 +296,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
|||||||
projectsRouter.Handle("/{id}/usage-limits", http.HandlerFunc(usageLimitsController.ProjectUsageLimits)).Methods(http.MethodGet, http.MethodOptions)
|
projectsRouter.Handle("/{id}/usage-limits", http.HandlerFunc(usageLimitsController.ProjectUsageLimits)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
projectsRouter.Handle("/usage-limits", http.HandlerFunc(usageLimitsController.TotalUsageLimits)).Methods(http.MethodGet, http.MethodOptions)
|
projectsRouter.Handle("/usage-limits", http.HandlerFunc(usageLimitsController.TotalUsageLimits)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
projectsRouter.Handle("/{id}/daily-usage", http.HandlerFunc(usageLimitsController.DailyUsage)).Methods(http.MethodGet, http.MethodOptions)
|
projectsRouter.Handle("/{id}/daily-usage", http.HandlerFunc(usageLimitsController.DailyUsage)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
projectsRouter.Handle("/total-usage-report", http.HandlerFunc(usageLimitsController.TotalUsageReport)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
authController := consoleapi.NewAuth(logger, service, accountFreezeService, mailService, server.cookieAuth, server.analytics, config.SatelliteName, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL, config.GeneralRequestURL)
|
authController := consoleapi.NewAuth(logger, service, accountFreezeService, mailService, server.cookieAuth, server.analytics, config.SatelliteName, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL, config.GeneralRequestURL)
|
||||||
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
|
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
|
||||||
|
@ -2684,6 +2684,38 @@ func (s *Service) GetAllBucketNames(ctx context.Context, projectID uuid.UUID) (_
|
|||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTotalUsageReport retrieves usage rollups for every bucket of all user owned projects for a given period.
|
||||||
|
func (s *Service) GetTotalUsageReport(ctx context.Context, since, before time.Time) ([]accounting.BucketUsageRollup, error) {
|
||||||
|
var err error
|
||||||
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
|
user, err := s.getUserAndAuditLog(ctx, "get user report")
|
||||||
|
if err != nil {
|
||||||
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := s.store.Projects().GetOwn(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := make([]accounting.BucketUsageRollup, 0)
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
rollups, err := s.projectAccounting.GetBucketUsageRollups(ctx, p.ID, since, before)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range rollups {
|
||||||
|
rollups[i].ProjectID = p.PublicID
|
||||||
|
usage = append(usage, rollups[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usage, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GenGetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period for generated api.
|
// GenGetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period for generated api.
|
||||||
func (s *Service) GenGetBucketUsageRollups(ctx context.Context, reqProjectID uuid.UUID, since, before time.Time) (rollups []accounting.BucketUsageRollup, httpError api.HTTPError) {
|
func (s *Service) GenGetBucketUsageRollups(ctx context.Context, reqProjectID uuid.UUID, since, before time.Time) (rollups []accounting.BucketUsageRollup, httpError api.HTTPError) {
|
||||||
var err error
|
var err error
|
||||||
|
@ -727,7 +727,7 @@ func (db *ProjectAccounting) GetSingleBucketUsageRollup(ctx context.Context, pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *ProjectAccounting) getSingleBucketRollup(ctx context.Context, projectID uuid.UUID, bucket string, since, before time.Time) (*accounting.BucketUsageRollup, error) {
|
func (db *ProjectAccounting) getSingleBucketRollup(ctx context.Context, projectID uuid.UUID, bucket string, since, before time.Time) (*accounting.BucketUsageRollup, error) {
|
||||||
roullupsQuery := db.db.Rebind(`SELECT SUM(settled), SUM(inline), action
|
rollupsQuery := db.db.Rebind(`SELECT SUM(settled), SUM(inline), action
|
||||||
FROM bucket_bandwidth_rollups
|
FROM bucket_bandwidth_rollups
|
||||||
WHERE project_id = ? AND bucket_name = ? AND interval_start >= ? AND interval_start <= ?
|
WHERE project_id = ? AND bucket_name = ? AND interval_start >= ? AND interval_start <= ?
|
||||||
GROUP BY action`)
|
GROUP BY action`)
|
||||||
@ -743,7 +743,7 @@ func (db *ProjectAccounting) getSingleBucketRollup(ctx context.Context, projectI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get bucket_bandwidth_rollup
|
// get bucket_bandwidth_rollup
|
||||||
rollupRows, err := db.db.QueryContext(ctx, roullupsQuery, projectID[:], []byte(bucket), since, before)
|
rollupRows, err := db.db.QueryContext(ctx, rollupsQuery, projectID[:], []byte(bucket), since, before)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -199,6 +199,17 @@ export class ProjectsHttpApi implements ProjectsApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get link to download total usage report for all the projects that user owns.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public getTotalUsageReportLink(start: Date, end: Date): string {
|
||||||
|
const since = Time.toUnixTimestamp(start).toString();
|
||||||
|
const before = Time.toUnixTimestamp(end).toString();
|
||||||
|
return `${this.ROOT_PATH}/total-usage-report?since=${since}&before=${before}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project daily usage for specific date range.
|
* Get project daily usage for specific date range.
|
||||||
*
|
*
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See LICENSE for copying information.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<div class="total-cost">
|
<div class="total-cost">
|
||||||
<div class="total-cost__header-container">
|
<div class="total-cost__header-container">
|
||||||
<h3 class="total-cost__header-container__title">Total Cost</h3>
|
<h3 class="total-cost__header-container__title">Total Cost</h3>
|
||||||
@ -52,6 +51,18 @@
|
|||||||
<p class="total-cost__card__label-text">Legacy STORJ Payments and Bonuses</p>
|
<p class="total-cost__card__label-text">Legacy STORJ Payments and Bonuses</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="total-cost__report">
|
||||||
|
<h3 class="total-cost__report__title">Detailed Usage Report</h3>
|
||||||
|
<p class="total-cost__report__info">Get a complete usage report for all your projects.</p>
|
||||||
|
<v-button
|
||||||
|
class="total-cost__report__button"
|
||||||
|
label="Download Report"
|
||||||
|
width="fit-content"
|
||||||
|
height="30px"
|
||||||
|
is-transparent
|
||||||
|
:on-press="downloadUsageReport"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isDataFetching">
|
<div v-if="isDataFetching">
|
||||||
<v-loader />
|
<v-loader />
|
||||||
@ -88,7 +99,6 @@
|
|||||||
/>
|
/>
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -104,6 +114,7 @@ import { useNotify } from '@/utils/hooks';
|
|||||||
import { useBillingStore } from '@/store/modules/billingStore';
|
import { useBillingStore } from '@/store/modules/billingStore';
|
||||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||||
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
||||||
|
import { Download } from '@/utils/download';
|
||||||
|
|
||||||
import UsageAndChargesItem from '@/components/account/billing/billingTabs/UsageAndChargesItem.vue';
|
import UsageAndChargesItem from '@/components/account/billing/billingTabs/UsageAndChargesItem.vue';
|
||||||
import VButton from '@/components/common/VButton.vue';
|
import VButton from '@/components/common/VButton.vue';
|
||||||
@ -172,6 +183,14 @@ function balanceClicked(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles download usage report click logic.
|
||||||
|
*/
|
||||||
|
function downloadUsageReport(): void {
|
||||||
|
const link = projectsStore.getTotalUsageReportLink();
|
||||||
|
Download.fileByLink(link);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle hook after initial render.
|
* Lifecycle hook after initial render.
|
||||||
* Fetches projects and usage rollup.
|
* Fetches projects and usage rollup.
|
||||||
@ -208,11 +227,32 @@ onMounted(async () => {
|
|||||||
font-family: 'font_regular', sans-serif;
|
font-family: 'font_regular', sans-serif;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&__report {
|
||||||
|
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
&__title,
|
||||||
|
&__info {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__header-container {
|
&__header-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&__date {
|
&__date {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -230,7 +270,6 @@ onMounted(async () => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
@media screen and (width <= 786px) {
|
@media screen and (width <= 786px) {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
@ -49,6 +49,14 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
|
|
||||||
const api: ProjectsApi = new ProjectsHttpApi();
|
const api: ProjectsApi = new ProjectsHttpApi();
|
||||||
|
|
||||||
|
function getTotalUsageReportLink(): string {
|
||||||
|
const now = new Date();
|
||||||
|
const endUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes()));
|
||||||
|
const startUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0));
|
||||||
|
|
||||||
|
return api.getTotalUsageReportLink(startUTC, endUTC);
|
||||||
|
}
|
||||||
|
|
||||||
async function getProjects(): Promise<Project[]> {
|
async function getProjects(): Promise<Project[]> {
|
||||||
const projects = await api.get();
|
const projects = await api.get();
|
||||||
|
|
||||||
@ -341,6 +349,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
requestLimitIncrease,
|
requestLimitIncrease,
|
||||||
getProjectLimits,
|
getProjectLimits,
|
||||||
getTotalLimits,
|
getTotalLimits,
|
||||||
|
getTotalUsageReportLink,
|
||||||
getProjectSalt,
|
getProjectSalt,
|
||||||
getUserInvitations,
|
getUserInvitations,
|
||||||
respondToInvitation,
|
respondToInvitation,
|
||||||
|
@ -64,6 +64,13 @@ export interface ProjectsApi {
|
|||||||
*/
|
*/
|
||||||
getTotalLimits(): Promise<ProjectLimits>;
|
getTotalLimits(): Promise<ProjectLimits>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get link to download total usage report for all the projects that user owns.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
getTotalUsageReportLink(start: Date, end: Date): string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project daily usage by specific date range.
|
* Get project daily usage by specific date range.
|
||||||
*
|
*
|
||||||
|
@ -14,4 +14,10 @@ export class Download {
|
|||||||
elem.click();
|
elem.click();
|
||||||
document.body.removeChild(elem);
|
document.body.removeChild(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static fileByLink(link: string): void {
|
||||||
|
const elem = window.document.createElement('a');
|
||||||
|
elem.href = link;
|
||||||
|
elem.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user