satellite/{console, web}: detailed usage report for a single project
Reworked usage report endpoint to return CSV for a single OR all the project user owns. Added buttons to download usage report CSV for a single project. Issue: https://github.com/storj/storj/issues/6154 Change-Id: I55104088180dcf6be49dcde6c9c495f07ba01c5a
This commit is contained in:
parent
f0f73fc8ae
commit
f6e357be52
@ -165,9 +165,17 @@ type BucketUsageRollup struct {
|
||||
Before time.Time `json:"before"`
|
||||
}
|
||||
|
||||
// ProjectBucketUsageRollup is total bucket usage info with project details for certain period.
|
||||
type ProjectBucketUsageRollup struct {
|
||||
ProjectName string `json:"projectName"`
|
||||
|
||||
BucketUsageRollup
|
||||
}
|
||||
|
||||
// ToStringSlice converts rollup values to a slice of strings.
|
||||
func (b *BucketUsageRollup) ToStringSlice() []string {
|
||||
func (b *ProjectBucketUsageRollup) ToStringSlice() []string {
|
||||
return []string{
|
||||
b.ProjectName,
|
||||
b.ProjectID.String(),
|
||||
b.BucketName,
|
||||
fmt.Sprintf("%f", b.TotalStoredData),
|
||||
|
@ -106,8 +106,8 @@ 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) {
|
||||
// UsageReport returns usage report for all the projects that user owns or a single user's project.
|
||||
func (ul *UsageLimits) UsageReport(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
@ -126,7 +126,18 @@ func (ul *UsageLimits) TotalUsageReport(w http.ResponseWriter, r *http.Request)
|
||||
since := time.Unix(sinceStamp, 0).UTC()
|
||||
before := time.Unix(beforeStamp, 0).UTC()
|
||||
|
||||
usage, err := ul.service.GetTotalUsageReport(ctx, since, before)
|
||||
var projectID uuid.UUID
|
||||
|
||||
idParam := r.URL.Query().Get("projectID")
|
||||
if idParam != "" {
|
||||
projectID, err = uuid.FromString(idParam)
|
||||
if err != nil {
|
||||
ul.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("invalid project id: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
usage, err := ul.service.GetUsageReport(ctx, since, before, projectID)
|
||||
if err != nil {
|
||||
if console.ErrUnauthorized.Has(err) {
|
||||
ul.serveJSONError(ctx, w, http.StatusUnauthorized, err)
|
||||
@ -137,14 +148,15 @@ func (ul *UsageLimits) TotalUsageReport(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "storj-report-" + since.Format("2006-01-02") + "-to-" + before.Format("2006-01-02") + ".csv"
|
||||
dateFormat := "2006-01-02"
|
||||
fileName := "storj-report-" + idParam + "-" + since.Format(dateFormat) + "-to-" + before.Format(dateFormat) + ".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"}
|
||||
csvHeaders := []string{"ProjectName", "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 {
|
||||
|
@ -235,29 +235,48 @@ func Test_TotalUsageReport(t *testing.T) {
|
||||
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)
|
||||
endpoint := fmt.Sprintf("projects/usage-report?since=%s&before=%s&projectID=", 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))
|
||||
reader := csv.NewReader(strings.NewReader(string(body)))
|
||||
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"}
|
||||
expectedHeaders := []string{"ProjectName", "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, project1.Name, records[1][0])
|
||||
require.Equal(t, project2.Name, records[2][0])
|
||||
require.Equal(t, project1.PublicID.String(), records[1][1])
|
||||
require.Equal(t, project2.PublicID.String(), records[2][1])
|
||||
require.Equal(t, bucketName, records[1][2])
|
||||
require.Equal(t, bucketName, records[2][2])
|
||||
for i := 3; i < 10; i++ {
|
||||
require.Equal(t, expectedCSVValue, records[1][i])
|
||||
require.Equal(t, expectedCSVValue, records[2][i])
|
||||
}
|
||||
|
||||
endpoint = fmt.Sprintf("projects/usage-report?since=%s&before=%s&projectID=%s", since, before, project1.PublicID)
|
||||
body, status, err = doRequestWithAuth(ctx, t, satelliteSys, user, http.MethodGet, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
|
||||
reader = csv.NewReader(strings.NewReader(string(body)))
|
||||
records, err = reader.ReadAll()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, records, 2)
|
||||
|
||||
require.Equal(t, project1.Name, records[1][0])
|
||||
require.Equal(t, project1.PublicID.String(), records[1][1])
|
||||
require.Equal(t, bucketName, records[1][2])
|
||||
|
||||
for i := 3; i < 10; i++ {
|
||||
require.Equal(t, expectedCSVValue, records[1][i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -297,7 +297,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("/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("/total-usage-report", http.HandlerFunc(usageLimitsController.TotalUsageReport)).Methods(http.MethodGet, http.MethodOptions)
|
||||
projectsRouter.Handle("/usage-report", http.HandlerFunc(usageLimitsController.UsageReport)).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)
|
||||
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
|
||||
|
@ -2723,22 +2723,35 @@ func (s *Service) GetAllBucketNames(ctx context.Context, projectID uuid.UUID) (_
|
||||
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) {
|
||||
// GetUsageReport retrieves usage rollups for every bucket of a single or all the user owned projects for a given period.
|
||||
func (s *Service) GetUsageReport(ctx context.Context, since, before time.Time, projectID uuid.UUID) ([]accounting.ProjectBucketUsageRollup, error) {
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
user, err := s.getUserAndAuditLog(ctx, "get user report")
|
||||
user, err := s.getUserAndAuditLog(ctx, "get usage report")
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
projects, err := s.store.Projects().GetOwn(ctx, user.ID)
|
||||
var projects []Project
|
||||
|
||||
if projectID.IsZero() {
|
||||
pr, err := s.store.Projects().GetOwn(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
usage := make([]accounting.BucketUsageRollup, 0)
|
||||
projects = append(projects, pr...)
|
||||
} else {
|
||||
_, pr, err := s.isProjectOwner(ctx, user.ID, projectID)
|
||||
if err != nil {
|
||||
return nil, ErrUnauthorized.Wrap(err)
|
||||
}
|
||||
|
||||
projects = append(projects, *pr)
|
||||
}
|
||||
|
||||
usage := make([]accounting.ProjectBucketUsageRollup, 0)
|
||||
|
||||
for _, p := range projects {
|
||||
rollups, err := s.projectAccounting.GetBucketUsageRollups(ctx, p.ID, since, before)
|
||||
@ -2746,9 +2759,23 @@ func (s *Service) GetTotalUsageReport(ctx context.Context, since, before time.Ti
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
for i := range rollups {
|
||||
rollups[i].ProjectID = p.PublicID
|
||||
usage = append(usage, rollups[i])
|
||||
for _, r := range rollups {
|
||||
usage = append(usage, accounting.ProjectBucketUsageRollup{
|
||||
ProjectName: p.Name,
|
||||
BucketUsageRollup: accounting.BucketUsageRollup{
|
||||
ProjectID: p.PublicID,
|
||||
BucketName: r.BucketName,
|
||||
TotalStoredData: r.TotalStoredData,
|
||||
TotalSegments: r.TotalSegments,
|
||||
ObjectCount: r.ObjectCount,
|
||||
MetadataSize: r.MetadataSize,
|
||||
RepairEgress: r.RepairEgress,
|
||||
GetEgress: r.GetEgress,
|
||||
AuditEgress: r.AuditEgress,
|
||||
Since: r.Since,
|
||||
Before: r.Before,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,10 +204,10 @@ export class ProjectsHttpApi implements ProjectsApi {
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public getTotalUsageReportLink(start: Date, end: Date): string {
|
||||
public getTotalUsageReportLink(start: Date, end: Date, projectID: string): string {
|
||||
const since = Time.toUnixTimestamp(start).toString();
|
||||
const before = Time.toUnixTimestamp(end).toString();
|
||||
return `${this.ROOT_PATH}/total-usage-report?since=${since}&before=${before}`;
|
||||
return `${this.ROOT_PATH}/usage-report?since=${since}&before=${before}&projectID=${projectID}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,8 +187,9 @@ function balanceClicked(): void {
|
||||
* Handles download usage report click logic.
|
||||
*/
|
||||
function downloadUsageReport(): void {
|
||||
const link = projectsStore.getTotalUsageReportLink();
|
||||
const link = projectsStore.getUsageReportLink();
|
||||
Download.fileByLink(link);
|
||||
notify.success('Usage report download started successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,6 +51,15 @@
|
||||
<p class="price">{{ centsToDollars(charge.segmentPrice) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-button
|
||||
class="usage-charges-item-container__detailed-info-container__btn"
|
||||
label="Download Report"
|
||||
width="140px"
|
||||
height="30px"
|
||||
font-size="14px"
|
||||
is-transparent
|
||||
:on-press="downloadUsageReport"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -66,6 +75,10 @@ import { Size } from '@/utils/bytesSize';
|
||||
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
|
||||
import { useBillingStore } from '@/store/modules/billingStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { Download } from '@/utils/download';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
|
||||
import VButton from '@/components/common/VButton.vue';
|
||||
|
||||
import GreyChevron from '@/../static/images/common/greyChevron.svg';
|
||||
|
||||
@ -86,6 +99,8 @@ const props = withDefaults(defineProps<{
|
||||
const billingStore = useBillingStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const notify = useNotify();
|
||||
|
||||
/**
|
||||
* isDetailedInfoShown indicates if area with detailed information about project charges is expanded.
|
||||
*/
|
||||
@ -118,6 +133,15 @@ const projectCharges = computed((): ProjectCharges => {
|
||||
return billingStore.state.projectCharges as ProjectCharges;
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles download usage report click logic.
|
||||
*/
|
||||
function downloadUsageReport(): void {
|
||||
const link = projectsStore.getUsageReportLink(props.projectId);
|
||||
Download.fileByLink(link);
|
||||
notify.success('Usage report download started successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns project usage price model from store.
|
||||
*/
|
||||
@ -325,6 +349,10 @@ function toggleDetailedInfo(): void {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,12 +49,12 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
|
||||
const api: ProjectsApi = new ProjectsHttpApi();
|
||||
|
||||
function getTotalUsageReportLink(): string {
|
||||
function getUsageReportLink(projectID = ''): 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);
|
||||
return api.getTotalUsageReportLink(startUTC, endUTC, projectID);
|
||||
}
|
||||
|
||||
async function getProjects(): Promise<Project[]> {
|
||||
@ -349,7 +349,7 @@ export const useProjectsStore = defineStore('projects', () => {
|
||||
requestLimitIncrease,
|
||||
getProjectLimits,
|
||||
getTotalLimits,
|
||||
getTotalUsageReportLink,
|
||||
getUsageReportLink,
|
||||
getProjectSalt,
|
||||
getUserInvitations,
|
||||
respondToInvitation,
|
||||
|
@ -69,7 +69,7 @@ export interface ProjectsApi {
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
getTotalUsageReportLink(start: Date, end: Date): string
|
||||
getTotalUsageReportLink(start: Date, end: Date, projectID: string): string
|
||||
|
||||
/**
|
||||
* Get project daily usage by specific date range.
|
||||
|
@ -63,6 +63,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<v-btn class="mt-4 ml-4" variant="outlined" color="default" size="small" @click="downloadReport">
|
||||
Download Report
|
||||
</v-btn>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
@ -72,6 +75,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
VBtn,
|
||||
VCard,
|
||||
VCol,
|
||||
VExpansionPanel,
|
||||
@ -89,6 +93,8 @@ import { Size } from '@/utils/bytesSize';
|
||||
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
|
||||
import { useBillingStore } from '@/store/modules/billingStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { Download } from '@/utils/download';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
|
||||
/**
|
||||
* HOURS_IN_MONTH constant shows amount of hours in 30-day month.
|
||||
@ -107,6 +113,8 @@ const props = withDefaults(defineProps<{
|
||||
const billingStore = useBillingStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const notify = useNotify();
|
||||
|
||||
/**
|
||||
* An array of tuples containing the partner name and usage charge for the specified project ID.
|
||||
*/
|
||||
@ -134,6 +142,15 @@ const projectCharges = computed((): ProjectCharges => {
|
||||
return billingStore.state.projectCharges as ProjectCharges;
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles download usage report click logic.
|
||||
*/
|
||||
function downloadReport(): void {
|
||||
const link = projectsStore.getUsageReportLink(props.projectId);
|
||||
Download.fileByLink(link);
|
||||
notify.success('Usage report download started successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns project usage price model from store.
|
||||
*/
|
||||
|
@ -330,8 +330,9 @@ const isCouponActive = computed((): boolean => {
|
||||
});
|
||||
|
||||
function downloadReport(): void {
|
||||
const link = projectsStore.getTotalUsageReportLink();
|
||||
const link = projectsStore.getUsageReportLink();
|
||||
Download.fileByLink(link);
|
||||
notify.success('Usage report download started successfully.');
|
||||
}
|
||||
|
||||
function goToTransactionsTab() {
|
||||
|
Loading…
Reference in New Issue
Block a user