satellite/console: new endpoint to get total usage and limits for all the projects user owns
Added new endpoint and service method to return total usage and limits for all the projects that user owns. It is needed for new paid tier UI Change-Id: Ic5b67ca7b275ec4930d976a007168235c0500b70
This commit is contained in:
parent
4031336cbd
commit
d30fd77652
@ -16,7 +16,7 @@ import (
|
||||
|
||||
var (
|
||||
// ErrAPIKeysAPI - console api keys api error type.
|
||||
ErrAPIKeysAPI = errs.Class("consoleapi keys")
|
||||
ErrAPIKeysAPI = errs.Class("console api keys")
|
||||
)
|
||||
|
||||
// APIKeys is an api controller that exposes all api keys related functionality.
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
|
||||
var (
|
||||
// ErrBucketsAPI - console buckets api error type.
|
||||
ErrBucketsAPI = errs.Class("consoleapi buckets")
|
||||
ErrBucketsAPI = errs.Class("console api buckets")
|
||||
)
|
||||
|
||||
// Buckets is an api controller that exposes all buckets related functionality.
|
||||
|
120
satellite/console/consoleweb/consoleapi/usageLimits.go
Normal file
120
satellite/console/consoleweb/consoleapi/usageLimits.go
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUsageLimitsAPI - console usage and limits api error type.
|
||||
ErrUsageLimitsAPI = errs.Class("console usage and limits")
|
||||
)
|
||||
|
||||
// UsageLimits is an api controller that exposes all usage and limits related functionality.
|
||||
type UsageLimits struct {
|
||||
log *zap.Logger
|
||||
service *console.Service
|
||||
}
|
||||
|
||||
// NewUsageLimits is a constructor for api usage and limits controller.
|
||||
func NewUsageLimits(log *zap.Logger, service *console.Service) *UsageLimits {
|
||||
return &UsageLimits{
|
||||
log: log,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// ProjectUsageLimits returns usage and limits by project ID.
|
||||
func (ul *UsageLimits) ProjectUsageLimits(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
usageLimits, err := ul.service.GetProjectUsageLimits(ctx, projectID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case console.ErrUnauthorized.Has(err):
|
||||
ul.serveJSONError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
case accounting.ErrInvalidArgument.Has(err):
|
||||
ul.serveJSONError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
default:
|
||||
ul.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(usageLimits)
|
||||
if err != nil {
|
||||
ul.log.Error("error encoding project usage limits", zap.Error(ErrUsageLimitsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
// TotalUsageLimits returns total usage and limits for all the projects that user owns.
|
||||
func (ul *UsageLimits) TotalUsageLimits(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
usageLimits, err := ul.service.GetTotalUsageLimits(ctx)
|
||||
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(usageLimits)
|
||||
if err != nil {
|
||||
ul.log.Error("error encoding project usage limits", zap.Error(ErrUsageLimitsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
||||
|
||||
// serveJSONError writes JSON error to response output stream.
|
||||
func (ul *UsageLimits) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||
ul.log.Error("returning error to client", zap.Int("code", status), zap.Error(err))
|
||||
|
||||
w.WriteHeader(status)
|
||||
|
||||
var response struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
response.Error = err.Error()
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
if err != nil {
|
||||
ul.log.Error("failed to write json error response", zap.Error(ErrUsageLimitsAPI.Wrap(err)))
|
||||
}
|
||||
}
|
115
satellite/console/consoleweb/consoleapi/usageLimits_test.go
Normal file
115
satellite/console/consoleweb/consoleapi/usageLimits_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
func Test_TotalUsageLimits(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, 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) {
|
||||
sat := planet.Satellites[0]
|
||||
|
||||
newUser := console.CreateUser{
|
||||
FullName: "Usage Limit Test",
|
||||
ShortName: "",
|
||||
Email: "ul@test.test",
|
||||
}
|
||||
|
||||
user, err := sat.AddUser(ctx, newUser, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
project0, err := sat.AddProject(ctx, user.ID, "testProject0")
|
||||
require.NoError(t, err)
|
||||
|
||||
project1, err := sat.AddProject(ctx, user.ID, "testProject1")
|
||||
require.NoError(t, err)
|
||||
|
||||
project2, err := sat.AddProject(ctx, user.ID, "testProject2")
|
||||
require.NoError(t, err)
|
||||
|
||||
const expectedLimit = 15
|
||||
|
||||
err = sat.DB.ProjectAccounting().UpdateProjectUsageLimit(ctx, project0.ID, expectedLimit)
|
||||
require.NoError(t, err)
|
||||
err = sat.DB.ProjectAccounting().UpdateProjectBandwidthLimit(ctx, project0.ID, expectedLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sat.DB.ProjectAccounting().UpdateProjectUsageLimit(ctx, project1.ID, expectedLimit)
|
||||
require.NoError(t, err)
|
||||
err = sat.DB.ProjectAccounting().UpdateProjectBandwidthLimit(ctx, project1.ID, expectedLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sat.DB.ProjectAccounting().UpdateProjectUsageLimit(ctx, project2.ID, expectedLimit)
|
||||
require.NoError(t, err)
|
||||
err = sat.DB.ProjectAccounting().UpdateProjectBandwidthLimit(ctx, project2.ID, expectedLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
// we are using full name as a password
|
||||
token, err := sat.API.Console.Service.Token(ctx, user.Email, user.FullName)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
"GET",
|
||||
"http://"+planet.Satellites[0].API.Console.Listener.Addr().String()+"/api/v0/projects/usage-limits",
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
expire := time.Now().AddDate(0, 0, 1)
|
||||
cookie := http.Cookie{
|
||||
Name: "_tokenKey",
|
||||
Path: "/",
|
||||
Value: token,
|
||||
Expires: expire,
|
||||
}
|
||||
|
||||
req.AddCookie(&cookie)
|
||||
|
||||
result, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(result.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output console.ProjectUsageLimits
|
||||
|
||||
err = json.Unmarshal(body, &output)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, int64(0), output.BandwidthUsed)
|
||||
require.Equal(t, int64(0), output.StorageUsed)
|
||||
require.Equal(t, int64(expectedLimit*3), output.BandwidthLimit)
|
||||
require.Equal(t, int64(expectedLimit*3), output.StorageLimit)
|
||||
|
||||
defer func() {
|
||||
err = result.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
})
|
||||
}
|
@ -33,7 +33,6 @@ import (
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/web"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/analytics"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
@ -206,9 +205,14 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
||||
|
||||
router.Handle("/api/v0/graphql", server.withAuth(http.HandlerFunc(server.graphqlHandler)))
|
||||
|
||||
usageLimitsController := consoleapi.NewUsageLimits(logger, service)
|
||||
router.Handle(
|
||||
"/api/v0/projects/{id}/usage-limits",
|
||||
server.withAuth(http.HandlerFunc(server.projectUsageLimitsHandler)),
|
||||
server.withAuth(http.HandlerFunc(usageLimitsController.ProjectUsageLimits)),
|
||||
).Methods(http.MethodGet)
|
||||
router.Handle(
|
||||
"/api/v0/projects/usage-limits",
|
||||
server.withAuth(http.HandlerFunc(usageLimitsController.TotalUsageLimits)),
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, partners, server.analytics, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
|
||||
@ -621,67 +625,6 @@ func (server *Server) cancelPasswordRecoveryHandler(w http.ResponseWriter, r *ht
|
||||
http.Redirect(w, r, "https://storjlabs.atlassian.net/servicedesk/customer/portals", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// projectUsageLimitsHandler api handler for project usage limits.
|
||||
func (server *Server) projectUsageLimitsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := error(nil)
|
||||
ctx := r.Context()
|
||||
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
var ok bool
|
||||
var idParam string
|
||||
|
||||
handleError := func(code int, err error) {
|
||||
w.WriteHeader(code)
|
||||
|
||||
var jsonError struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// N.B. we are probably leaking internal details to the client
|
||||
jsonError.Error = err.Error()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(jsonError); err != nil {
|
||||
server.log.Error("error encoding project usage limits error", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceError := func(err error) {
|
||||
switch {
|
||||
case console.ErrUnauthorized.Has(err):
|
||||
handleError(http.StatusUnauthorized, err)
|
||||
case accounting.ErrInvalidArgument.Has(err):
|
||||
handleError(http.StatusBadRequest, err)
|
||||
default:
|
||||
handleError(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if idParam, ok = mux.Vars(r)["id"]; !ok {
|
||||
handleError(http.StatusBadRequest, errs.New("missing project id route param"))
|
||||
return
|
||||
}
|
||||
|
||||
projectID, err := uuid.FromString(idParam)
|
||||
if err != nil {
|
||||
handleError(http.StatusBadRequest, errs.New("invalid project id: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
limits, err := server.service.GetProjectUsageLimits(ctx, projectID)
|
||||
if err != nil {
|
||||
handleServiceError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(limits); err != nil {
|
||||
server.log.Error("error encoding project usage limits", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// graphqlHandler is graphql endpoint http handler function.
|
||||
func (server *Server) graphqlHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
@ -1517,27 +1517,86 @@ func (s *Service) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID
|
||||
func (s *Service) GetProjectUsageLimits(ctx context.Context, projectID uuid.UUID) (_ *ProjectUsageLimits, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
_, err = s.getAuthAndAuditLog(ctx, "get project usage limits", zap.String("projectID", projectID.String()))
|
||||
auth, err := s.getAuthAndAuditLog(ctx, "get project usage limits", zap.String("projectID", projectID.String()))
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
storageLimit, err := s.projectUsage.GetProjectStorageLimit(ctx, projectID)
|
||||
if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
prUsageLimits, err := s.getProjectUsageLimits(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
bandwidthLimit, err := s.projectUsage.GetProjectBandwidthLimit(ctx, projectID)
|
||||
|
||||
return &ProjectUsageLimits{
|
||||
StorageLimit: prUsageLimits.StorageLimit,
|
||||
BandwidthLimit: prUsageLimits.BandwidthLimit,
|
||||
StorageUsed: prUsageLimits.StorageUsed,
|
||||
BandwidthUsed: prUsageLimits.BandwidthUsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTotalUsageLimits returns total limits and current usage for all the projects.
|
||||
func (s *Service) GetTotalUsageLimits(ctx context.Context) (_ *ProjectUsageLimits, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
auth, err := s.getAuthAndAuditLog(ctx, "get total usage and limits for all the projects")
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
projects, err := s.store.Projects().GetOwn(ctx, auth.User.ID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
var totalStorageLimit int64
|
||||
var totalBandwidthLimit int64
|
||||
var totalStorageUsed int64
|
||||
var totalBandwidthUsed int64
|
||||
|
||||
for _, pr := range projects {
|
||||
prUsageLimits, err := s.getProjectUsageLimits(ctx, pr.ID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
totalStorageLimit += prUsageLimits.StorageLimit
|
||||
totalBandwidthLimit += prUsageLimits.BandwidthLimit
|
||||
totalStorageUsed += prUsageLimits.StorageUsed
|
||||
totalBandwidthUsed += prUsageLimits.BandwidthUsed
|
||||
}
|
||||
|
||||
return &ProjectUsageLimits{
|
||||
StorageLimit: totalStorageLimit,
|
||||
BandwidthLimit: totalBandwidthLimit,
|
||||
StorageUsed: totalStorageUsed,
|
||||
BandwidthUsed: totalBandwidthUsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID) (_ *ProjectUsageLimits, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
storageLimit, err := s.projectUsage.GetProjectStorageLimit(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bandwidthLimit, err := s.projectUsage.GetProjectBandwidthLimit(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
return nil, err
|
||||
}
|
||||
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ProjectUsageLimits{
|
||||
|
@ -151,6 +151,33 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
||||
throw new Error('can not get usage limits');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total limits for all the projects that user owns.
|
||||
*
|
||||
* throws Error
|
||||
*/
|
||||
public async getTotalLimits(): Promise<ProjectLimits> {
|
||||
const path = `${this.ROOT_PATH}/usage-limits`;
|
||||
const response = await this.http.get(path);
|
||||
|
||||
if (response.ok) {
|
||||
const limits = await response.json();
|
||||
|
||||
return new ProjectLimits(
|
||||
limits.bandwidthLimit,
|
||||
limits.bandwidthUsed,
|
||||
limits.storageLimit,
|
||||
limits.storageUsed,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('can not get total usage limits');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch owned projects.
|
||||
*
|
||||
|
@ -22,6 +22,7 @@ export const PROJECTS_ACTIONS = {
|
||||
DELETE: 'deleteProject',
|
||||
CLEAR: 'clearProjects',
|
||||
GET_LIMITS: 'getProjectLimits',
|
||||
GET_TOTAL_LIMITS: 'getTotalLimits',
|
||||
};
|
||||
|
||||
export const PROJECTS_MUTATIONS = {
|
||||
@ -33,6 +34,7 @@ export const PROJECTS_MUTATIONS = {
|
||||
SELECT_PROJECT: 'SELECT_PROJECT',
|
||||
CLEAR_PROJECTS: 'CLEAR_PROJECTS',
|
||||
SET_LIMITS: 'SET_PROJECT_LIMITS',
|
||||
SET_TOTAL_LIMITS: 'SET_TOTAL_LIMITS',
|
||||
SET_PAGE_NUMBER: 'SET_PAGE_NUMBER',
|
||||
SET_PAGE: 'SET_PAGE',
|
||||
};
|
||||
@ -43,6 +45,7 @@ export class ProjectsState {
|
||||
public projects: Project[] = [];
|
||||
public selectedProject: Project = defaultSelectedProject;
|
||||
public currentLimits: ProjectLimits = new ProjectLimits();
|
||||
public totalLimits: ProjectLimits = new ProjectLimits();
|
||||
public cursor: ProjectsCursor = new ProjectsCursor();
|
||||
public page: ProjectsPage = new ProjectsPage();
|
||||
}
|
||||
@ -57,6 +60,7 @@ const {
|
||||
DELETE,
|
||||
CLEAR,
|
||||
GET_LIMITS,
|
||||
GET_TOTAL_LIMITS,
|
||||
FETCH_OWNED,
|
||||
} = PROJECTS_ACTIONS;
|
||||
|
||||
@ -69,6 +73,7 @@ const {
|
||||
SELECT_PROJECT,
|
||||
CLEAR_PROJECTS,
|
||||
SET_LIMITS,
|
||||
SET_TOTAL_LIMITS,
|
||||
SET_PAGE_NUMBER,
|
||||
SET_PAGE,
|
||||
} = PROJECTS_MUTATIONS;
|
||||
@ -129,6 +134,9 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
|
||||
[SET_LIMITS](state: ProjectsState, limits: ProjectLimits): void {
|
||||
state.currentLimits = limits;
|
||||
},
|
||||
[SET_TOTAL_LIMITS](state: ProjectsState, limits: ProjectLimits): void {
|
||||
state.totalLimits = limits;
|
||||
},
|
||||
[CLEAR_PROJECTS](state: ProjectsState): void {
|
||||
state.projects = [];
|
||||
state.selectedProject = defaultSelectedProject;
|
||||
@ -202,6 +210,13 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
|
||||
|
||||
return limits;
|
||||
},
|
||||
[GET_TOTAL_LIMITS]: async function ({commit}: any): Promise<ProjectLimits> {
|
||||
const limits = await api.getTotalLimits();
|
||||
|
||||
commit(SET_TOTAL_LIMITS, limits);
|
||||
|
||||
return limits;
|
||||
},
|
||||
[CLEAR]: function({commit}: any): void {
|
||||
commit(CLEAR_PROJECTS);
|
||||
},
|
||||
|
@ -45,6 +45,13 @@ export interface ProjectsApi {
|
||||
*/
|
||||
getLimits(projectId: string): Promise<ProjectLimits>;
|
||||
|
||||
/**
|
||||
* Get project limits.
|
||||
*
|
||||
* throws Error
|
||||
*/
|
||||
getTotalLimits(): Promise<ProjectLimits>;
|
||||
|
||||
/**
|
||||
* Fetch owned projects.
|
||||
*
|
||||
|
@ -42,4 +42,8 @@ export class ProjectsApiMock implements ProjectsApi {
|
||||
getLimits(projectId: string): Promise<ProjectLimits> {
|
||||
return Promise.resolve(this.mockLimits);
|
||||
}
|
||||
|
||||
getTotalLimits(): Promise<ProjectLimits> {
|
||||
return Promise.resolve(this.mockLimits);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user