Console buckets page (#1847)
This commit is contained in:
parent
ae24778ef7
commit
2d2301d5ff
@ -18,8 +18,17 @@ const (
|
||||
ProjectInputType = "projectInput"
|
||||
// ProjectUsageType is a graphql type name for project usage
|
||||
ProjectUsageType = "projectUsage"
|
||||
// BucketUsageCursorInputType is a graphql input
|
||||
// type name for bucket usage cursor
|
||||
BucketUsageCursorInputType = "bucketUsageCursor"
|
||||
// BucketUsageType is a graphql type name for bucket usage
|
||||
BucketUsageType = "bucketUsage"
|
||||
// BucketUsagePageType is a field name for bucket usage page
|
||||
BucketUsagePageType = "bucketUsagePage"
|
||||
// FieldName is a field name for "name"
|
||||
FieldName = "name"
|
||||
// FieldBucketName is a field name for "bucket name"
|
||||
FieldBucketName = "bucketName"
|
||||
// FieldDescription is a field name for description
|
||||
FieldDescription = "description"
|
||||
// FieldMembers is field name for members
|
||||
@ -28,12 +37,24 @@ const (
|
||||
FieldAPIKeys = "apiKeys"
|
||||
// FieldUsage is a field name for usage rollup
|
||||
FieldUsage = "usage"
|
||||
// FieldBucketUsages is a field name for bucket usages
|
||||
FieldBucketUsages = "bucketUsages"
|
||||
// FieldStorage is a field name for storage total
|
||||
FieldStorage = "storage"
|
||||
// FieldEgress is a field name for egress total
|
||||
FieldEgress = "egress"
|
||||
// FieldObjectCount is a field name for objects count
|
||||
FieldObjectCount = "objectCount"
|
||||
// FieldPageCount is a field name for total page count
|
||||
FieldPageCount = "pageCount"
|
||||
// FieldCurrentPage is a field name for current page number
|
||||
FieldCurrentPage = "currentPage"
|
||||
// FieldTotalCount is a field name for bucket usage count total
|
||||
FieldTotalCount = "totalCount"
|
||||
// CursorArg is an argument name for cursor
|
||||
CursorArg = "cursor"
|
||||
// PageArg ia an argument name for page number
|
||||
PageArg = "page"
|
||||
// LimitArg is argument name for limit
|
||||
LimitArg = "limit"
|
||||
// OffsetArg is argument name for offset
|
||||
@ -149,6 +170,25 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec
|
||||
return service.GetProjectUsage(p.Context, project.ID, since, before)
|
||||
},
|
||||
},
|
||||
FieldBucketUsages: &graphql.Field{
|
||||
Type: types.bucketUsagePage,
|
||||
Args: graphql.FieldConfigArgument{
|
||||
BeforeArg: &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.DateTime),
|
||||
},
|
||||
CursorArg: &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(types.bucketUsageCursor),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
project, _ := p.Source.(*console.Project)
|
||||
|
||||
before := p.Args[BeforeArg].(time.Time)
|
||||
cursor := fromMapBucketUsageCursor(p.Args[CursorArg].(map[string]interface{}))
|
||||
|
||||
return service.GetBucketTotals(p.Context, project.ID, cursor, before)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -168,6 +208,81 @@ func graphqlProjectInput() *graphql.InputObject {
|
||||
})
|
||||
}
|
||||
|
||||
// graphqlBucketUsageCursor creates bucket usage cursor graphql input type
|
||||
func graphqlBucketUsageCursor() *graphql.InputObject {
|
||||
return graphql.NewInputObject(graphql.InputObjectConfig{
|
||||
Name: BucketUsageCursorInputType,
|
||||
Fields: graphql.InputObjectConfigFieldMap{
|
||||
SearchArg: &graphql.InputObjectFieldConfig{
|
||||
Type: graphql.NewNonNull(graphql.String),
|
||||
},
|
||||
LimitArg: &graphql.InputObjectFieldConfig{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
PageArg: &graphql.InputObjectFieldConfig{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// graphqlBucketUsage creates bucket usage grapqhl type
|
||||
func graphqlBucketUsage() *graphql.Object {
|
||||
return graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: BucketUsageType,
|
||||
Fields: graphql.Fields{
|
||||
FieldBucketName: &graphql.Field{
|
||||
Type: graphql.String,
|
||||
},
|
||||
FieldStorage: &graphql.Field{
|
||||
Type: graphql.Float,
|
||||
},
|
||||
FieldEgress: &graphql.Field{
|
||||
Type: graphql.Float,
|
||||
},
|
||||
FieldObjectCount: &graphql.Field{
|
||||
Type: graphql.Float,
|
||||
},
|
||||
SinceArg: &graphql.Field{
|
||||
Type: graphql.DateTime,
|
||||
},
|
||||
BeforeArg: &graphql.Field{
|
||||
Type: graphql.DateTime,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// graphqlBucketUsagePage creates bucket usage page graphql object
|
||||
func graphqlBucketUsagePage(types *TypeCreator) *graphql.Object {
|
||||
return graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: BucketUsagePageType,
|
||||
Fields: graphql.Fields{
|
||||
FieldBucketUsages: &graphql.Field{
|
||||
Type: graphql.NewList(types.bucketUsage),
|
||||
},
|
||||
SearchArg: &graphql.Field{
|
||||
Type: graphql.String,
|
||||
},
|
||||
LimitArg: &graphql.Field{
|
||||
Type: graphql.Int,
|
||||
},
|
||||
OffsetArg: &graphql.Field{
|
||||
Type: graphql.Int,
|
||||
},
|
||||
FieldPageCount: &graphql.Field{
|
||||
Type: graphql.Int,
|
||||
},
|
||||
FieldCurrentPage: &graphql.Field{
|
||||
Type: graphql.Int,
|
||||
},
|
||||
FieldTotalCount: &graphql.Field{
|
||||
Type: graphql.Int,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// graphqlProjectUsage creates project usage graphql type
|
||||
func graphqlProjectUsage() *graphql.Object {
|
||||
return graphql.NewObject(graphql.ObjectConfig{
|
||||
@ -192,10 +307,21 @@ func graphqlProjectUsage() *graphql.Object {
|
||||
})
|
||||
}
|
||||
|
||||
// fromMapProjectInfo creates satellite.ProjectInfo from input args
|
||||
// fromMapProjectInfo creates console.ProjectInfo from input args
|
||||
func fromMapProjectInfo(args map[string]interface{}) (project console.ProjectInfo) {
|
||||
project.Name, _ = args[FieldName].(string)
|
||||
project.Description, _ = args[FieldDescription].(string)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// fromMapBucketUsageCursor creates console.BucketUsageCursor from input args
|
||||
func fromMapBucketUsageCursor(args map[string]interface{}) (cursor console.BucketUsageCursor) {
|
||||
limit, _ := args[LimitArg].(int)
|
||||
page, _ := args[PageArg].(int)
|
||||
|
||||
cursor.Limit = uint(limit)
|
||||
cursor.Page = uint(page)
|
||||
cursor.Search, _ = args[SearchArg].(string)
|
||||
return
|
||||
}
|
||||
|
@ -18,15 +18,18 @@ type TypeCreator struct {
|
||||
|
||||
token *graphql.Object
|
||||
|
||||
user *graphql.Object
|
||||
project *graphql.Object
|
||||
projectUsage *graphql.Object
|
||||
projectMember *graphql.Object
|
||||
apiKeyInfo *graphql.Object
|
||||
createAPIKey *graphql.Object
|
||||
user *graphql.Object
|
||||
project *graphql.Object
|
||||
projectUsage *graphql.Object
|
||||
bucketUsage *graphql.Object
|
||||
bucketUsagePage *graphql.Object
|
||||
projectMember *graphql.Object
|
||||
apiKeyInfo *graphql.Object
|
||||
createAPIKey *graphql.Object
|
||||
|
||||
userInput *graphql.InputObject
|
||||
projectInput *graphql.InputObject
|
||||
userInput *graphql.InputObject
|
||||
projectInput *graphql.InputObject
|
||||
bucketUsageCursor *graphql.InputObject
|
||||
}
|
||||
|
||||
// Create create types and check for error
|
||||
@ -42,6 +45,11 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ
|
||||
return err
|
||||
}
|
||||
|
||||
c.bucketUsageCursor = graphqlBucketUsageCursor()
|
||||
if err := c.bucketUsageCursor.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// entities
|
||||
c.user = graphqlUser()
|
||||
if err := c.user.Error(); err != nil {
|
||||
@ -53,6 +61,16 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ
|
||||
return err
|
||||
}
|
||||
|
||||
c.bucketUsage = graphqlBucketUsage()
|
||||
if err := c.bucketUsage.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.bucketUsagePage = graphqlBucketUsagePage(c)
|
||||
if err := c.bucketUsagePage.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.apiKeyInfo = graphqlAPIKeyInfo()
|
||||
if err := c.apiKeyInfo.Error(); err != nil {
|
||||
return err
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
monkit "gopkg.in/spacemonkeygo/monkit.v2"
|
||||
"gopkg.in/spacemonkeygo/monkit.v2"
|
||||
|
||||
"storj.io/storj/pkg/auth"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
@ -806,6 +806,24 @@ func (s *Service) GetProjectUsage(ctx context.Context, projectID uuid.UUID, sinc
|
||||
return projectUsage, nil
|
||||
}
|
||||
|
||||
// GetBucketTotals retrieves paged bucket total usages since project creation
|
||||
func (s *Service) GetBucketTotals(ctx context.Context, projectID uuid.UUID, cursor BucketUsageCursor, before time.Time) (*BucketUsagePage, error) {
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
auth, err := GetAuth(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isMember, err := s.isProjectMember(ctx, auth.User.ID, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.store.UsageRollups().GetBucketTotals(ctx, projectID, cursor, isMember.project.CreatedAt, before)
|
||||
}
|
||||
|
||||
// GetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period
|
||||
func (s *Service) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since, before time.Time) ([]BucketUsageRollup, error) {
|
||||
var err error
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
type UsageRollups interface {
|
||||
GetProjectTotal(ctx context.Context, projectID uuid.UUID, since, before time.Time) (*ProjectUsage, error)
|
||||
GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since, before time.Time) ([]BucketUsageRollup, error)
|
||||
GetBucketTotals(ctx context.Context, projectID uuid.UUID, cursor BucketUsageCursor, since, before time.Time) (*BucketUsagePage, error)
|
||||
}
|
||||
|
||||
// ProjectUsage consist of period total storage, egress
|
||||
@ -27,6 +28,40 @@ type ProjectUsage struct {
|
||||
Before time.Time
|
||||
}
|
||||
|
||||
// BucketUsage consist of total bucket usage for period
|
||||
type BucketUsage struct {
|
||||
ProjectID uuid.UUID
|
||||
BucketName string
|
||||
|
||||
Storage float64
|
||||
Egress float64
|
||||
ObjectCount float64
|
||||
|
||||
Since time.Time
|
||||
Before time.Time
|
||||
}
|
||||
|
||||
// BucketUsageCursor holds info for bucket usage
|
||||
// cursor pagination
|
||||
type BucketUsageCursor struct {
|
||||
Search string
|
||||
Limit uint
|
||||
Page uint
|
||||
}
|
||||
|
||||
// BucketUsagePage represents bucket usage page result
|
||||
type BucketUsagePage struct {
|
||||
BucketUsages []BucketUsage
|
||||
|
||||
Search string
|
||||
Limit uint
|
||||
Offset uint64
|
||||
|
||||
PageCount uint
|
||||
CurrentPage uint
|
||||
TotalCount uint64
|
||||
}
|
||||
|
||||
// BucketUsageRollup is total bucket usage info
|
||||
// for certain period
|
||||
type BucketUsageRollup struct {
|
||||
|
@ -1,17 +1,228 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package console
|
||||
package console_test
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"storj.io/storj/internal/testcontext"
|
||||
"storj.io/storj/pkg/accounting"
|
||||
"storj.io/storj/pkg/pb"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/satellitedb/satellitedbtest"
|
||||
)
|
||||
|
||||
const (
|
||||
numBuckets = 5
|
||||
tallyIntervals = 10
|
||||
|
||||
tallyInterval = time.Hour
|
||||
)
|
||||
|
||||
func TestUsageRollups(t *testing.T) {
|
||||
fmt.Println(time.Now().Format(time.RFC3339))
|
||||
// 2018-04-02T18:25:11+03:00
|
||||
// 2020-04-05T18:25:11+03:00
|
||||
// 2019-04-03 17:16:26.822857431+00:00
|
||||
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
|
||||
ctx := testcontext.New(t)
|
||||
defer ctx.Cleanup()
|
||||
|
||||
now := time.Now()
|
||||
start := now.Add(tallyInterval * time.Duration(-tallyIntervals))
|
||||
|
||||
project1, err := uuid.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
project2, err := uuid.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p1base := binary.BigEndian.Uint64(project1[:8]) >> 48
|
||||
p2base := binary.BigEndian.Uint64(project2[:8]) >> 48
|
||||
|
||||
getValue := func(i, j int, base uint64) int64 {
|
||||
a := uint64((i+1)*(j+1)) ^ base
|
||||
a &^= (1 << 63)
|
||||
return int64(a)
|
||||
}
|
||||
|
||||
actions := []pb.PieceAction{
|
||||
pb.PieceAction_GET,
|
||||
pb.PieceAction_GET_AUDIT,
|
||||
pb.PieceAction_GET_REPAIR,
|
||||
}
|
||||
|
||||
var buckets []string
|
||||
for i := 0; i < numBuckets; i++ {
|
||||
bucketName := fmt.Sprintf("bucket_%d", i)
|
||||
|
||||
// project 1
|
||||
bucketID := []byte(project1.String() + "/" + bucketName)
|
||||
for _, action := range actions {
|
||||
value := getValue(0, i, p1base)
|
||||
|
||||
err := db.Orders().UpdateBucketBandwidthAllocation(ctx,
|
||||
bucketID,
|
||||
action,
|
||||
value*6,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Orders().UpdateBucketBandwidthSettle(ctx,
|
||||
bucketID,
|
||||
action,
|
||||
value*3,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Orders().UpdateBucketBandwidthInline(ctx,
|
||||
bucketID,
|
||||
action,
|
||||
value,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// project 2
|
||||
bucketID = []byte(project2.String() + "/" + bucketName)
|
||||
for _, action := range actions {
|
||||
value := getValue(1, i, p2base)
|
||||
|
||||
err := db.Orders().UpdateBucketBandwidthAllocation(ctx,
|
||||
bucketID,
|
||||
action,
|
||||
value*6,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Orders().UpdateBucketBandwidthSettle(ctx,
|
||||
bucketID,
|
||||
action,
|
||||
value*3,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Orders().UpdateBucketBandwidthInline(ctx,
|
||||
bucketID,
|
||||
action,
|
||||
value,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
buckets = append(buckets, bucketName)
|
||||
}
|
||||
|
||||
for i := 0; i < tallyIntervals; i++ {
|
||||
interval := start.Add(tallyInterval * time.Duration(i))
|
||||
|
||||
bucketTallies := make(map[string]*accounting.BucketTally)
|
||||
for j, bucket := range buckets {
|
||||
bucketID1 := project1.String() + "/" + bucket
|
||||
bucketID2 := project2.String() + "/" + bucket
|
||||
value1 := getValue(i, j, p1base) * 10
|
||||
value2 := getValue(i, j, p2base) * 10
|
||||
|
||||
tally1 := &accounting.BucketTally{
|
||||
Segments: value1,
|
||||
InlineSegments: value1,
|
||||
RemoteSegments: value1,
|
||||
Files: value1,
|
||||
InlineFiles: value1,
|
||||
RemoteFiles: value1,
|
||||
Bytes: value1,
|
||||
InlineBytes: value1,
|
||||
RemoteBytes: value1,
|
||||
MetadataSize: value1,
|
||||
}
|
||||
|
||||
tally2 := &accounting.BucketTally{
|
||||
Segments: value2,
|
||||
InlineSegments: value2,
|
||||
RemoteSegments: value2,
|
||||
Files: value2,
|
||||
InlineFiles: value2,
|
||||
RemoteFiles: value2,
|
||||
Bytes: value2,
|
||||
InlineBytes: value2,
|
||||
RemoteBytes: value2,
|
||||
MetadataSize: value2,
|
||||
}
|
||||
|
||||
bucketTallies[bucketID1] = tally1
|
||||
bucketTallies[bucketID2] = tally2
|
||||
}
|
||||
|
||||
tallies, err := db.ProjectAccounting().SaveTallies(ctx, interval, bucketTallies)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tallies) != len(buckets)*2 {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
usageRollups := db.Console().UsageRollups()
|
||||
|
||||
t.Run("test project total", func(t *testing.T) {
|
||||
projTotal1, err := usageRollups.GetProjectTotal(ctx, *project1, start, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, projTotal1)
|
||||
|
||||
projTotal2, err := usageRollups.GetProjectTotal(ctx, *project2, start, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, projTotal2)
|
||||
})
|
||||
|
||||
t.Run("test bucket usage rollups", func(t *testing.T) {
|
||||
rollups1, err := usageRollups.GetBucketUsageRollups(ctx, *project1, start, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rollups1)
|
||||
|
||||
rollups2, err := usageRollups.GetBucketUsageRollups(ctx, *project2, start, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rollups2)
|
||||
})
|
||||
|
||||
t.Run("test bucket totals", func(t *testing.T) {
|
||||
cursor := console.BucketUsageCursor{
|
||||
Limit: 20,
|
||||
Page: 1,
|
||||
}
|
||||
|
||||
totals1, err := usageRollups.GetBucketTotals(ctx, *project1, cursor, start, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, totals1)
|
||||
|
||||
totals2, err := usageRollups.GetBucketTotals(ctx, *project2, cursor, start, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, totals2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -415,6 +415,12 @@ type lockedUsageRollups struct {
|
||||
db console.UsageRollups
|
||||
}
|
||||
|
||||
func (m *lockedUsageRollups) GetBucketTotals(ctx context.Context, projectID uuid.UUID, cursor console.BucketUsageCursor, since time.Time, before time.Time) (*console.BucketUsagePage, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.db.GetBucketTotals(ctx, projectID, cursor, since, before)
|
||||
}
|
||||
|
||||
func (m *lockedUsageRollups) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since time.Time, before time.Time) ([]console.BucketUsageRollup, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
@ -23,6 +23,8 @@ type usagerollups struct {
|
||||
|
||||
// GetProjectTotal retrieves project usage for a given period
|
||||
func (db *usagerollups) GetProjectTotal(ctx context.Context, projectID uuid.UUID, since, before time.Time) (usage *console.ProjectUsage, err error) {
|
||||
since = timeTruncateDown(since)
|
||||
|
||||
storageQuery := db.db.All_BucketStorageTally_By_ProjectId_And_BucketName_And_IntervalStart_GreaterOrEqual_And_IntervalStart_LessOrEqual_OrderBy_Desc_IntervalStart
|
||||
|
||||
roullupsQuery := db.db.Rebind(`SELECT SUM(settled), SUM(inline), action
|
||||
@ -95,6 +97,8 @@ func (db *usagerollups) GetProjectTotal(ctx context.Context, projectID uuid.UUID
|
||||
|
||||
// GetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period
|
||||
func (db *usagerollups) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since, before time.Time) ([]console.BucketUsageRollup, error) {
|
||||
since = timeTruncateDown(since)
|
||||
|
||||
buckets, err := db.getBuckets(ctx, projectID, since, before)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -177,6 +181,154 @@ func (db *usagerollups) GetBucketUsageRollups(ctx context.Context, projectID uui
|
||||
return bucketUsageRollups, nil
|
||||
}
|
||||
|
||||
// GetBucketTotals retrieves bucket usage totals for period of time
|
||||
func (db *usagerollups) GetBucketTotals(ctx context.Context, projectID uuid.UUID, cursor console.BucketUsageCursor, since, before time.Time) (*console.BucketUsagePage, error) {
|
||||
since = timeTruncateDown(since)
|
||||
search := cursor.Search + "%"
|
||||
|
||||
if cursor.Limit > 50 {
|
||||
cursor.Limit = 50
|
||||
}
|
||||
if cursor.Page == 0 {
|
||||
return nil, errs.New("page can not be 0")
|
||||
}
|
||||
|
||||
page := &console.BucketUsagePage{
|
||||
Search: cursor.Search,
|
||||
Limit: cursor.Limit,
|
||||
Offset: uint64((cursor.Page - 1) * cursor.Limit),
|
||||
}
|
||||
|
||||
countQuery := db.db.Rebind(`SELECT COUNT(DISTINCT bucket_name)
|
||||
FROM bucket_bandwidth_rollups
|
||||
WHERE project_id = ? AND interval_start >= ? AND interval_start <= ?
|
||||
AND CAST(bucket_name as TEXT) LIKE ?`)
|
||||
|
||||
countRow := db.db.QueryRowContext(ctx,
|
||||
countQuery,
|
||||
[]byte(projectID.String()),
|
||||
since, before,
|
||||
search)
|
||||
|
||||
err := countRow.Scan(&page.TotalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if page.TotalCount == 0 {
|
||||
return page, nil
|
||||
}
|
||||
if page.Offset > page.TotalCount-1 {
|
||||
return nil, errs.New("page is out of range")
|
||||
}
|
||||
|
||||
bucketsQuery := db.db.Rebind(`SELECT DISTINCT bucket_name
|
||||
FROM bucket_bandwidth_rollups
|
||||
WHERE project_id = ? AND interval_start >= ? AND interval_start <= ?
|
||||
AND CAST(bucket_name as TEXT) LIKE ?
|
||||
ORDER BY bucket_name ASC
|
||||
LIMIT ? OFFSET ?`)
|
||||
|
||||
bucketRows, err := db.db.QueryContext(ctx,
|
||||
bucketsQuery,
|
||||
[]byte(projectID.String()),
|
||||
since, before,
|
||||
search,
|
||||
page.Limit,
|
||||
page.Offset)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, bucketRows.Close()) }()
|
||||
|
||||
var buckets []string
|
||||
for bucketRows.Next() {
|
||||
var bucket string
|
||||
err = bucketRows.Scan(&bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buckets = append(buckets, bucket)
|
||||
}
|
||||
|
||||
roullupsQuery := db.db.Rebind(`SELECT SUM(settled), SUM(inline), action
|
||||
FROM bucket_bandwidth_rollups
|
||||
WHERE project_id = ? AND bucket_name = ? AND interval_start >= ? AND interval_start <= ?
|
||||
GROUP BY action`)
|
||||
|
||||
storageQuery := db.db.All_BucketStorageTally_By_ProjectId_And_BucketName_And_IntervalStart_GreaterOrEqual_And_IntervalStart_LessOrEqual_OrderBy_Desc_IntervalStart
|
||||
|
||||
var bucketUsages []console.BucketUsage
|
||||
for _, bucket := range buckets {
|
||||
bucketUsage := console.BucketUsage{
|
||||
ProjectID: projectID,
|
||||
BucketName: bucket,
|
||||
Since: since,
|
||||
Before: before,
|
||||
}
|
||||
|
||||
// get bucket_bandwidth_rollups
|
||||
rollupsRows, err := db.db.QueryContext(ctx, roullupsQuery, []byte(projectID.String()), []byte(bucket), since, before)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, rollupsRows.Close()) }()
|
||||
|
||||
var totalEgress int64
|
||||
for rollupsRows.Next() {
|
||||
var action pb.PieceAction
|
||||
var settled, inline int64
|
||||
|
||||
err = rollupsRows.Scan(&settled, &inline, &action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add values for egress
|
||||
if action == pb.PieceAction_GET || action == pb.PieceAction_GET_AUDIT || action == pb.PieceAction_GET_REPAIR {
|
||||
totalEgress += settled + inline
|
||||
}
|
||||
}
|
||||
|
||||
bucketUsage.Egress = memory.Size(totalEgress).GB()
|
||||
|
||||
bucketStorageTallies, err := storageQuery(ctx,
|
||||
dbx.BucketStorageTally_ProjectId([]byte(projectID.String())),
|
||||
dbx.BucketStorageTally_BucketName([]byte(bucket)),
|
||||
dbx.BucketStorageTally_IntervalStart(since),
|
||||
dbx.BucketStorageTally_IntervalStart(before))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fill metadata, objects and stored data
|
||||
// hours calculated from previous tallies,
|
||||
// so we skip the most recent one
|
||||
for i := len(bucketStorageTallies) - 1; i > 0; i-- {
|
||||
current := bucketStorageTallies[i]
|
||||
|
||||
hours := bucketStorageTallies[i-1].IntervalStart.Sub(current.IntervalStart).Hours()
|
||||
|
||||
bucketUsage.Storage += memory.Size(current.Remote).GB() * hours
|
||||
bucketUsage.Storage += memory.Size(current.Inline).GB() * hours
|
||||
bucketUsage.ObjectCount += float64(current.ObjectCount) * hours
|
||||
}
|
||||
|
||||
bucketUsages = append(bucketUsages, bucketUsage)
|
||||
}
|
||||
|
||||
page.PageCount = uint(page.TotalCount / uint64(cursor.Limit))
|
||||
if page.TotalCount%uint64(cursor.Limit) != 0 {
|
||||
page.PageCount++
|
||||
}
|
||||
|
||||
page.BucketUsages = bucketUsages
|
||||
page.CurrentPage = cursor.Page
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// getBuckets list all bucket of certain project for given period
|
||||
func (db *usagerollups) getBuckets(ctx context.Context, projectID uuid.UUID, since, before time.Time) ([]string, error) {
|
||||
bucketsQuery := db.db.Rebind(`SELECT DISTINCT bucket_name
|
||||
@ -202,3 +354,8 @@ func (db *usagerollups) getBuckets(ctx context.Context, projectID uuid.UUID, sin
|
||||
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
// timeTruncateDown truncates down to the hour before to be in sync with orders endpoint
|
||||
func timeTruncateDown(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
|
||||
}
|
||||
|
@ -41,3 +41,57 @@ export async function fetchProjectUsage(projectID: string, since: Date, before:
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// fetchBucketUsages retrieves bucket usage totals for a particular project
|
||||
export async function fetchBucketUsages(projectID: string, before: Date, cursor: BucketUsageCursor): Promise<RequestResponse<BucketUsagePage>> {
|
||||
let result: RequestResponse<BucketUsagePage> = {
|
||||
errorMessage: '',
|
||||
isSuccess: false,
|
||||
data: {} as BucketUsagePage
|
||||
};
|
||||
|
||||
let response: any = null;
|
||||
try {
|
||||
response = await apollo.query(
|
||||
{
|
||||
query: gql(`
|
||||
query {
|
||||
project(id: "${projectID}") {
|
||||
bucketUsages(before: "${before.toISOString()}", cursor: {
|
||||
limit: ${cursor.limit}, search: "${cursor.search}", page: ${cursor.page}
|
||||
}) {
|
||||
bucketUsages{
|
||||
bucketName,
|
||||
storage,
|
||||
egress,
|
||||
objectCount,
|
||||
since,
|
||||
before
|
||||
},
|
||||
search,
|
||||
limit,
|
||||
offset,
|
||||
pageCount,
|
||||
currentPage,
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}`
|
||||
),
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'all'
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (response.errors) {
|
||||
result.errorMessage = response.errors[0].message;
|
||||
} else {
|
||||
result.isSuccess = true;
|
||||
result.data = response.data.project.bucketUsages;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -3,21 +3,25 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="buckets > 0" class="buckets-overflow">
|
||||
<div class="buckets-overflow">
|
||||
<div class="buckets-header">
|
||||
<p>Buckets</p>
|
||||
<SearchArea/>
|
||||
</div>
|
||||
<div class="buckets-container">
|
||||
<div v-if="buckets.length > 0" class="buckets-container">
|
||||
<table style="width:98.5%; margin-top:20px;">
|
||||
<SortingHeader />
|
||||
<BucketItem />
|
||||
<BucketItem v-for="(bucket, index) in buckets" v-bind:bucket="bucket" v-bind:key="index" />
|
||||
</table>
|
||||
<PaginationArea />
|
||||
</div>
|
||||
<EmptyState
|
||||
v-if="pages === 0 && search && search.length > 0"
|
||||
mainTitle="Nothing found :("
|
||||
:imageSource="emptyImage" />
|
||||
</div>
|
||||
<EmptyState
|
||||
v-if="buckets === 0"
|
||||
v-if="pages === 0 && !search"
|
||||
mainTitle="You have no Buckets yet"
|
||||
:imageSource="emptyImage" />
|
||||
</div>
|
||||
@ -35,8 +39,7 @@
|
||||
@Component({
|
||||
data: function () {
|
||||
return {
|
||||
emptyImage: EMPTY_STATE_IMAGES.API_KEY,
|
||||
buckets: 1,
|
||||
emptyImage: EMPTY_STATE_IMAGES.API_KEY
|
||||
};
|
||||
},
|
||||
components: {
|
||||
@ -45,7 +48,18 @@
|
||||
SortingHeader,
|
||||
BucketItem,
|
||||
PaginationArea,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
buckets: function (): BucketUsage[] {
|
||||
return this.$store.state.bucketUsageModule.page.bucketUsages;
|
||||
},
|
||||
pages: function (): number {
|
||||
return this.$store.state.bucketUsageModule.page.pageCount;
|
||||
},
|
||||
search: function (): string {
|
||||
return this.$store.state.bucketUsageModule.cursor.search;
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class BucketArea extends Vue {}
|
||||
</script>
|
||||
|
@ -3,18 +3,32 @@
|
||||
|
||||
<template>
|
||||
<tr class="container">
|
||||
<td class="container__item">test</td>
|
||||
<td class="container__item">test</td>
|
||||
<td class="container__item">test</td>
|
||||
<td class="container__item">test</td>
|
||||
<td class="container__item">test</td>
|
||||
<td class="container__item">{{ bucket.bucketName }}</td>
|
||||
<td class="container__item">{{ storage }}</td>
|
||||
<td class="container__item">{{ egress }}</td>
|
||||
<td class="container__item">{{ objectCount }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component({})
|
||||
@Component({
|
||||
props: {
|
||||
bucket: Object
|
||||
},
|
||||
computed: {
|
||||
storage: function (): string {
|
||||
return (this as any).bucket.storage.toFixed(4);
|
||||
},
|
||||
egress: function (): string {
|
||||
return (this as any).bucket.egress.toFixed(4);
|
||||
},
|
||||
objectCount: function (): string {
|
||||
return (this as any).bucket.objectCount.toFixed(4);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default class BucketItem extends Vue {}
|
||||
</script>
|
||||
|
@ -5,18 +5,14 @@
|
||||
<div>
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-container__pages">
|
||||
<div v-html="arrowLeft" class="pagination-container__button"></div>
|
||||
<div v-html="arrowLeft" v-on:click="prevPage" class="pagination-container__button"></div>
|
||||
<div class="pagination-container__items">
|
||||
<span class="selected">1</span>
|
||||
<span>2</span>
|
||||
<span>3</span>
|
||||
<span>4</span>
|
||||
<span>5</span>
|
||||
<span v-for="(value, index) in pages" v-bind:class="isSelected(index+1)" v-on:click="onPageClick($event, index+1)">{{index+1}}</span>
|
||||
</div>
|
||||
<div v-html="arrowRight" class="pagination-container__button"></div>
|
||||
<div v-html="arrowRight" v-on:click="nextPage" class="pagination-container__button"></div>
|
||||
</div>
|
||||
<div class="pagination-container__counter">
|
||||
<p>Showing <span>1</span> to <span>6</span> of <span>90</span> entries.</p>
|
||||
<p>Showing <span>{{firstEdge}}</span> to <span>{{lastEdge}}</span> of <span>{{totalCount}}</span> entries.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,6 +21,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { EMPTY_STATE_IMAGES } from '@/utils/constants/emptyStatesImages';
|
||||
import { BUCKET_USAGE_ACTIONS, NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
|
||||
|
||||
@Component({
|
||||
data: function() {
|
||||
@ -33,6 +30,66 @@
|
||||
arrowRight: EMPTY_STATE_IMAGES.ARROW_RIGHT,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onPageClick: async function (event: any, page: number) {
|
||||
const response = await this.$store.dispatch(BUCKET_USAGE_ACTIONS.FETCH, page);
|
||||
if (!response.isSuccess) {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch buckets: ' + response.errorMessage);
|
||||
}
|
||||
},
|
||||
isSelected: function (page: number): string {
|
||||
return page === (this as any).currentPage ? "selected" : "";
|
||||
},
|
||||
nextPage: async function() {
|
||||
if ((this as any).isLastPage) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await this.$store.dispatch(BUCKET_USAGE_ACTIONS.FETCH, (this as any).currentPage + 1);
|
||||
if (!response.isSuccess) {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch buckets: ' + response.errorMessage);
|
||||
}
|
||||
},
|
||||
prevPage: async function() {
|
||||
if ((this as any).isFirstPage) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await this.$store.dispatch(BUCKET_USAGE_ACTIONS.FETCH, (this as any).currentPage - 1);
|
||||
if (!response.isSuccess) {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch buckets: ' + response.errorMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pages: function(): number[] {
|
||||
return new Array(this.$store.state.bucketUsageModule.page.pageCount);
|
||||
},
|
||||
currentPage: function (): number {
|
||||
return this.$store.state.bucketUsageModule.page.currentPage;
|
||||
},
|
||||
firstEdge: function (): number {
|
||||
return this.$store.state.bucketUsageModule.page.offset + 1;
|
||||
},
|
||||
lastEdge: function (): number {
|
||||
let offset = this.$store.state.bucketUsageModule.page.offset;
|
||||
let bucketsLength = this.$store.state.bucketUsageModule.page.bucketUsages.length;
|
||||
|
||||
return offset + bucketsLength;
|
||||
},
|
||||
totalCount: function (): number {
|
||||
return this.$store.state.bucketUsageModule.page.totalCount;
|
||||
},
|
||||
isFirstPage: function() {
|
||||
return this.$store.state.bucketUsageModule.page.currentPage === 1;
|
||||
},
|
||||
isLastPage: function (): boolean {
|
||||
let currentPage = this.$store.state.bucketUsageModule.page.currentPage;
|
||||
let pageCount = this.$store.state.bucketUsageModule.page.pageCount;
|
||||
|
||||
return currentPage === pageCount;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default class PaginationArea extends Vue {}
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="search-container">
|
||||
<div class="search-container__wrap">
|
||||
<label class="search-container__wrap__input">
|
||||
<input placeholder="Search Buckets" type="text">
|
||||
<input v-model="search" v-on:input="fetch" placeholder="Search Buckets" type="text">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -13,8 +13,28 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { BUCKET_USAGE_ACTIONS, NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
|
||||
|
||||
@Component({})
|
||||
@Component({
|
||||
methods: {
|
||||
fetch: async function() {
|
||||
const bucketsResponse = await this.$store.dispatch(BUCKET_USAGE_ACTIONS.FETCH, 1);
|
||||
if (!bucketsResponse.isSuccess) {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch buckets: ' + bucketsResponse.errorMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
search: {
|
||||
get: function (): string {
|
||||
return this.$store.state.bucketUsageModule.cursor.search;
|
||||
},
|
||||
set: function (search: string) {
|
||||
this.$store.dispatch(BUCKET_USAGE_ACTIONS.SET_SEARCH, search)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default class SearchArea extends Vue {}
|
||||
</script>
|
||||
|
@ -6,46 +6,21 @@
|
||||
<th class="sort-header-container__item">
|
||||
<div class="row">
|
||||
<p>Bucket Name</p>
|
||||
<div class="sort-header-container__item__arrows">
|
||||
<span v-html="arrowUp"></span>
|
||||
<span class="selected" v-html="arrowDown"></span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort-header-container__item">
|
||||
<div class="row">
|
||||
<p>Creation Date</p>
|
||||
<div class="sort-header-container__item__arrows">
|
||||
<span v-html="arrowUp"></span>
|
||||
<span v-html="arrowDown"></span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort-header-container__item">
|
||||
<div class="row">
|
||||
<p>Storage Used</p>
|
||||
<div class="sort-header-container__item__arrows">
|
||||
<span v-html="arrowUp"></span>
|
||||
<span v-html="arrowDown"></span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort-header-container__item">
|
||||
<div class="row">
|
||||
<p>Egress Used</p>
|
||||
<div class="sort-header-container__item__arrows">
|
||||
<span v-html="arrowUp"></span>
|
||||
<span v-html="arrowDown"></span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort-header-container__item">
|
||||
<div class="row">
|
||||
<p>Objects Stored</p>
|
||||
<div class="sort-header-container__item__arrows">
|
||||
<span v-html="arrowUp"></span>
|
||||
<span v-html="arrowDown"></span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
@ -53,15 +28,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { EMPTY_STATE_IMAGES } from '@/utils/constants/emptyStatesImages';
|
||||
@Component({
|
||||
data: function() {
|
||||
return {
|
||||
arrowUp: EMPTY_STATE_IMAGES.ARROW_UP,
|
||||
arrowDown: EMPTY_STATE_IMAGES.ARROW_DOWN,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
@Component({})
|
||||
|
||||
export default class SortBucketsHeader extends Vue {
|
||||
}
|
||||
</script>
|
||||
|
@ -188,13 +188,13 @@ import { toUnixTimestamp } from '@/utils/time';
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
storage: function () {
|
||||
storage: function (): string {
|
||||
return this.$store.state.usageModule.projectUsage.storage.toPrecision(5);
|
||||
},
|
||||
egress: function () {
|
||||
egress: function (): string {
|
||||
return this.$store.state.usageModule.projectUsage.egress.toPrecision(5);
|
||||
},
|
||||
objectsCount: function () {
|
||||
objectsCount: function (): string {
|
||||
return this.$store.state.usageModule.projectUsage.objectCount.toPrecision(5);
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +101,11 @@ let router = new Router({
|
||||
name: ROUTES.API_KEYS.name,
|
||||
component: ApiKeysArea
|
||||
},
|
||||
{
|
||||
path: ROUTES.BUCKETS.path,
|
||||
name: ROUTES.BUCKETS.name,
|
||||
component: BucketArea
|
||||
},
|
||||
// {
|
||||
// path: ROUTES.BUCKETS.path,
|
||||
// name: ROUTES.BUCKETS.name,
|
||||
@ -125,7 +130,7 @@ let router = new Router({
|
||||
// and if we are able to navigate to page without existing project
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (isUnavailablePageWithoutProject(to.name as string)) {
|
||||
next(ROUTES.PROJECT_OVERVIEW);
|
||||
next(ROUTES.PROJECT_OVERVIEW + '/' + ROUTES.PROJECT_DETAILS);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { projectMembersModule } from '@/store/modules/projectMembers';
|
||||
import { notificationsModule } from '@/store/modules/notifications';
|
||||
import { appStateModule } from '@/store/modules/appState';
|
||||
import { apiKeysModule } from '@/store/modules/apiKeys';
|
||||
import { usageModule } from '@/store/modules/usage';
|
||||
import { bucketUsageModule, usageModule } from '@/store/modules/usage';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@ -24,7 +24,8 @@ const store = new Vuex.Store({
|
||||
notificationsModule,
|
||||
appStateModule,
|
||||
apiKeysModule,
|
||||
usageModule
|
||||
usageModule,
|
||||
bucketUsageModule
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { PROJECT_USAGE_MUTATIONS } from '@/store/mutationConstants';
|
||||
import { PROJECT_USAGE_ACTIONS } from '@/utils/constants/actionNames';
|
||||
import { fetchProjectUsage } from '@/api/usage';
|
||||
import {BUCKET_USAGE_MUTATIONS, PROJECT_USAGE_MUTATIONS} from '@/store/mutationConstants';
|
||||
import {BUCKET_USAGE_ACTIONS, PROJECT_USAGE_ACTIONS} from '@/utils/constants/actionNames';
|
||||
import { fetchBucketUsages, fetchProjectUsage } from '@/api/usage';
|
||||
|
||||
export const usageModule = {
|
||||
state: {
|
||||
projectUsage: {storage: 0, egress: 0, objectCount: 0} as ProjectUsage
|
||||
projectUsage: {storage: 0, egress: 0, objectCount: 0} as ProjectUsage,
|
||||
},
|
||||
mutations: {
|
||||
[PROJECT_USAGE_MUTATIONS.FETCH](state: any, projectUsage: ProjectUsage) {
|
||||
@ -34,3 +34,50 @@ export const usageModule = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const bucketPageLimit = 12;
|
||||
const firstPage = 1;
|
||||
|
||||
export const bucketUsageModule = {
|
||||
state: {
|
||||
cursor: { limit: bucketPageLimit, search: '', page: firstPage } as BucketUsageCursor,
|
||||
page: { bucketUsages: [] as BucketUsage[] } as BucketUsagePage,
|
||||
},
|
||||
mutations: {
|
||||
[BUCKET_USAGE_MUTATIONS.FETCH](state: any, page: BucketUsagePage) {
|
||||
state.page = page;
|
||||
},
|
||||
[BUCKET_USAGE_MUTATIONS.SET_PAGE](state: any, page: number) {
|
||||
state.cursor.page = page;
|
||||
},
|
||||
[BUCKET_USAGE_MUTATIONS.SET_SEARCH](state: any, search: string) {
|
||||
state.cursor.search = search;
|
||||
},
|
||||
[BUCKET_USAGE_MUTATIONS.CLEAR](state: any) {
|
||||
state.cursor = { limit: bucketPageLimit, search: '', page: firstPage } as BucketUsageCursor;
|
||||
state.page = { bucketUsages: [] as BucketUsage[] } as BucketUsagePage;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
[BUCKET_USAGE_ACTIONS.FETCH]: async function({commit, rootGetters, state}: any, page: number): Promise<RequestResponse<BucketUsagePage>> {
|
||||
const projectID = rootGetters.selectedProject.id;
|
||||
const before = new Date();
|
||||
state.cursor.page = page;
|
||||
|
||||
commit(BUCKET_USAGE_MUTATIONS.SET_PAGE, page);
|
||||
|
||||
let result = await fetchBucketUsages(projectID, before, state.cursor);
|
||||
if (result.isSuccess) {
|
||||
commit(BUCKET_USAGE_MUTATIONS.FETCH, result.data);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[BUCKET_USAGE_ACTIONS.SET_SEARCH]: function({commit}, search: string) {
|
||||
commit(BUCKET_USAGE_MUTATIONS.SET_SEARCH, search);
|
||||
},
|
||||
[BUCKET_USAGE_ACTIONS.CLEAR]: function({commit}) {
|
||||
commit(BUCKET_USAGE_MUTATIONS.CLEAR);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -44,6 +44,13 @@ export const PROJECT_USAGE_MUTATIONS = {
|
||||
CLEAR: 'CLEAR_PROJECT_USAGE'
|
||||
};
|
||||
|
||||
export const BUCKET_USAGE_MUTATIONS = {
|
||||
FETCH: 'FETCH_BUCKET_USAGES',
|
||||
SET_SEARCH: "SET_SEARCH_BUCKET_USAGE",
|
||||
SET_PAGE: "SET_PAGE_BUCKET_USAGE",
|
||||
CLEAR: 'CLEAR_BUCKET_USAGES'
|
||||
};
|
||||
|
||||
export const NOTIFICATION_MUTATIONS = {
|
||||
ADD: 'ADD_NOTIFICATION',
|
||||
DELETE: 'DELETE_NOTIFICATION',
|
||||
|
9
web/satellite/src/types/projects.d.ts
vendored
9
web/satellite/src/types/projects.d.ts
vendored
@ -28,12 +28,3 @@ declare type TeamMemberModel = {
|
||||
}
|
||||
joinedAt: string,
|
||||
};
|
||||
|
||||
// ProjectUsage sums usage for given period
|
||||
declare type ProjectUsage = {
|
||||
storage: number,
|
||||
egress: number,
|
||||
objectCount: number,
|
||||
since: Date,
|
||||
before: Date
|
||||
};
|
||||
|
40
web/satellite/src/types/usage.d.ts
vendored
Normal file
40
web/satellite/src/types/usage.d.ts
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
// ProjectUsage sums usage for given period
|
||||
declare type ProjectUsage = {
|
||||
storage: number,
|
||||
egress: number,
|
||||
objectCount: number,
|
||||
since: Date,
|
||||
before: Date
|
||||
};
|
||||
|
||||
// BucketUSage total usage of a bucket for given period
|
||||
declare type BucketUsage = {
|
||||
bucketName: string,
|
||||
storage: number,
|
||||
egress: number,
|
||||
objectCount: number,
|
||||
since: Date,
|
||||
before: Date
|
||||
};
|
||||
|
||||
// BucketUsagePage holds bucket total usages and flag
|
||||
// wether more usages available
|
||||
declare type BucketUsagePage = {
|
||||
bucketUsages: BucketUsage[],
|
||||
search: string,
|
||||
limit: number,
|
||||
offset: number,
|
||||
pageCount: number,
|
||||
currentPage: number,
|
||||
totalCount: number,
|
||||
};
|
||||
|
||||
// BucketUsageCursor holds cursor for bucket name and limit
|
||||
declare type BucketUsageCursor = {
|
||||
search: string,
|
||||
limit: number,
|
||||
page: number
|
||||
};
|
@ -71,3 +71,9 @@ export const PROJECT_USAGE_ACTIONS = {
|
||||
FETCH: 'fetchProjectUsage',
|
||||
CLEAR: 'clearProjectUsage',
|
||||
};
|
||||
|
||||
export const BUCKET_USAGE_ACTIONS = {
|
||||
FETCH: 'fetchBucketUsages',
|
||||
SET_SEARCH: 'setSearchBucketUsage',
|
||||
CLEAR: 'clearBucketUsages'
|
||||
};
|
||||
|
@ -58,15 +58,15 @@ const NAVIGATION_ITEMS = {
|
||||
</defs>
|
||||
</svg>`
|
||||
},
|
||||
// BUCKETS: {
|
||||
// label: 'Buckets',
|
||||
// path: ROUTES.BUCKETS.path,
|
||||
// svg: `<svg class="svg" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
// <path d="M1 12.4548V20.273C1 21.273 1.81116 22.0003 2.71245 22.0003H20.1974C21.1888 22.0003 21.9099 21.1821 21.9099 20.273V12.4548C21.9099 11.4548 21.0987 10.7275 20.1974 10.7275H2.71245C1.81116 10.7275 1 11.4548 1 12.4548ZM14.97 14.0912H7.75966C7.30901 14.0912 6.85837 13.7275 6.85837 13.1821C6.85837 12.7275 7.21888 12.273 7.75966 12.273H14.97C15.4206 12.273 15.8712 12.6366 15.8712 13.1821C15.8712 13.7275 15.5107 14.0912 14.97 14.0912Z" fill="#354049"/>
|
||||
// <path d="M2.53227 9.81792C2.17175 9.81792 1.90137 9.54519 1.90137 9.18155V5.90882C1.90137 5.54519 2.17175 5.27246 2.53227 5.27246H20.4679C20.8284 5.27246 21.0988 5.54519 21.0988 5.90882V8.99973C21.0988 9.36337 20.8284 9.6361 20.4679 9.6361C20.1074 9.6361 19.837 9.36337 19.837 8.99973V6.54519H3.16317V9.18155C3.16317 9.54519 2.89278 9.81792 2.53227 9.81792Z" fill="#354049"/>
|
||||
// <path d="M20.4679 4.27273H2.53227C2.17175 4.27273 1.90137 4 1.90137 3.63636C1.90137 3.27273 2.17175 3 2.53227 3H20.4679C20.8284 3 21.0988 3.27273 21.0988 3.63636C21.0988 4 20.8284 4.27273 20.4679 4.27273Z" fill="#354049"/>
|
||||
// </svg>`
|
||||
// },
|
||||
BUCKETS: {
|
||||
label: 'Buckets',
|
||||
path: ROUTES.BUCKETS.path,
|
||||
svg: `<svg class="svg" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 12.4548V20.273C1 21.273 1.81116 22.0003 2.71245 22.0003H20.1974C21.1888 22.0003 21.9099 21.1821 21.9099 20.273V12.4548C21.9099 11.4548 21.0987 10.7275 20.1974 10.7275H2.71245C1.81116 10.7275 1 11.4548 1 12.4548ZM14.97 14.0912H7.75966C7.30901 14.0912 6.85837 13.7275 6.85837 13.1821C6.85837 12.7275 7.21888 12.273 7.75966 12.273H14.97C15.4206 12.273 15.8712 12.6366 15.8712 13.1821C15.8712 13.7275 15.5107 14.0912 14.97 14.0912Z" fill="#354049"/>
|
||||
<path d="M2.53227 9.81792C2.17175 9.81792 1.90137 9.54519 1.90137 9.18155V5.90882C1.90137 5.54519 2.17175 5.27246 2.53227 5.27246H20.4679C20.8284 5.27246 21.0988 5.54519 21.0988 5.90882V8.99973C21.0988 9.36337 20.8284 9.6361 20.4679 9.6361C20.1074 9.6361 19.837 9.36337 19.837 8.99973V6.54519H3.16317V9.18155C3.16317 9.54519 2.89278 9.81792 2.53227 9.81792Z" fill="#354049"/>
|
||||
<path d="M20.4679 4.27273H2.53227C2.17175 4.27273 1.90137 4 1.90137 3.63636C1.90137 3.27273 2.17175 3 2.53227 3H20.4679C20.8284 3 21.0988 3.27273 21.0988 3.63636C21.0988 4 20.8284 4.27273 20.4679 4.27273Z" fill="#354049"/>
|
||||
</svg>`
|
||||
},
|
||||
};
|
||||
|
||||
export default NAVIGATION_ITEMS;
|
||||
|
@ -35,6 +35,7 @@ import {AppState} from "../utils/constants/appStateEnum";
|
||||
PROJETS_ACTIONS,
|
||||
USER_ACTIONS,
|
||||
PROJECT_USAGE_ACTIONS,
|
||||
BUCKET_USAGE_ACTIONS
|
||||
} from '@/utils/constants/actionNames';
|
||||
import ROUTES from '@/utils/constants/routerConstants';
|
||||
import ProjectCreationSuccessPopup from '@/components/project/ProjectCreationSuccessPopup.vue';
|
||||
@ -83,6 +84,11 @@ import {AppState} from "../utils/constants/appStateEnum";
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch project usage');
|
||||
}
|
||||
|
||||
const bucketsResponse = await this.$store.dispatch(BUCKET_USAGE_ACTIONS.FETCH, 1);
|
||||
if (!bucketsResponse.isSuccess) {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch buckets: ' + bucketsResponse.errorMessage);
|
||||
}
|
||||
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
|
||||
}, 800);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user