diff --git a/components/dashboard/src/Pagination/Pagination.tsx b/components/dashboard/src/Pagination/Pagination.tsx index ed0659f1a9abb6..b9a86d891cf083 100644 --- a/components/dashboard/src/Pagination/Pagination.tsx +++ b/components/dashboard/src/Pagination/Pagination.tsx @@ -8,20 +8,19 @@ import { getPaginationNumbers } from "./getPagination"; import Arrow from "../components/Arrow"; interface PaginationProps { - totalResults: number; totalNumberOfPages: number; currentPage: number; - setCurrentPage: any; + setPage: (page: number) => void; } -function Pagination({ totalNumberOfPages, currentPage, setCurrentPage }: PaginationProps) { +function Pagination({ totalNumberOfPages, currentPage, setPage }: PaginationProps) { const calculatedPagination = getPaginationNumbers(totalNumberOfPages, currentPage); const nextPage = () => { - if (currentPage !== totalNumberOfPages) setCurrentPage(currentPage + 1); + if (currentPage !== totalNumberOfPages) setPage(currentPage + 1); }; const prevPage = () => { - if (currentPage !== 1) setCurrentPage(currentPage - 1); + if (currentPage !== 1) setPage(currentPage - 1); }; const getClassnames = (pageNumber: string | number) => { if (pageNumber === currentPage) { @@ -47,8 +46,8 @@ function Pagination({ totalNumberOfPages, currentPage, setCurrentPage }: Paginat return
  • ; } return ( -
  • - setCurrentPage(pn)}>{pn} +
  • typeof pn === "number" && setPage(pn)}> + {pn}
  • ); })} diff --git a/components/dashboard/src/teams/TeamUsage.tsx b/components/dashboard/src/teams/TeamUsage.tsx index 05429d90c3981e..0ab9c1367ac470 100644 --- a/components/dashboard/src/teams/TeamUsage.tsx +++ b/components/dashboard/src/teams/TeamUsage.tsx @@ -9,10 +9,10 @@ import { useLocation } from "react-router"; import { getCurrentTeam, TeamsContext } from "./teams-context"; import { getGitpodService, gitpodHostUrl } from "../service/service"; import { - BillableSessionRequest, + ListBilledUsageRequest, BillableWorkspaceType, ExtendedBillableSession, - SortOrder, + ListBilledUsageResponse, } from "@gitpod/gitpod-protocol/lib/usage"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { Item, ItemField, ItemsList } from "../components/ItemsList"; @@ -21,7 +21,6 @@ import Header from "../components/Header"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { ReactComponent as CreditsSvg } from "../images/credits.svg"; import { ReactComponent as Spinner } from "../icons/Spinner.svg"; -import { ReactComponent as SortArrow } from "../images/sort-arrow.svg"; import { ReactComponent as UsageIcon } from "../images/usage-default.svg"; import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; import { toRemoteURL } from "../projects/render-utils"; @@ -31,17 +30,15 @@ function TeamUsage() { const location = useLocation(); const team = getCurrentTeam(location, teams); const [teamBillingMode, setTeamBillingMode] = useState(undefined); - const [billedUsage, setBilledUsage] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [resultsPerPage] = useState(50); + const [usagePage, setUsagePage] = useState(undefined); const [errorMessage, setErrorMessage] = useState(""); const today = new Date(); const startOfCurrentMonth = new Date(today.getFullYear(), today.getMonth(), 1); const timestampStartOfCurrentMonth = startOfCurrentMonth.getTime(); const [startDateOfBillMonth, setStartDateOfBillMonth] = useState(timestampStartOfCurrentMonth); const [endDateOfBillMonth, setEndDateOfBillMonth] = useState(Date.now()); + const [totalCreditsUsed, setTotalCreditsUsed] = useState(0); const [isLoading, setIsLoading] = useState(true); - const [isStartedTimeDescending, setIsStartedTimeDescending] = useState(true); useEffect(() => { if (!team) { @@ -57,30 +54,8 @@ function TeamUsage() { if (!team) { return; } - if (billedUsage.length === 0) { - setIsLoading(true); - } - (async () => { - const attributionId = AttributionId.render({ kind: "team", teamId: team.id }); - const request: BillableSessionRequest = { - attributionId, - startedTimeOrder: isStartedTimeDescending ? SortOrder.Descending : SortOrder.Ascending, - from: startDateOfBillMonth, - to: endDateOfBillMonth, - }; - try { - const { server } = getGitpodService(); - const billedUsageResult = await server.listBilledUsage(request); - setBilledUsage(billedUsageResult); - } catch (error) { - if (error.code === ErrorCodes.PERMISSION_DENIED) { - setErrorMessage("Access to usage details is restricted to team owners."); - } - } finally { - setIsLoading(false); - } - })(); - }, [team, startDateOfBillMonth, endDateOfBillMonth, isStartedTimeDescending]); + loadPage(1); + }, [team, startDateOfBillMonth, endDateOfBillMonth]); useEffect(() => { if (!teamBillingMode) { @@ -91,6 +66,37 @@ function TeamUsage() { } }, [teamBillingMode]); + const loadPage = async (page: number = 1) => { + if (!team) { + return; + } + if (usagePage === undefined) { + setIsLoading(true); + setTotalCreditsUsed(0); + } + const attributionId = AttributionId.render({ kind: "team", teamId: team.id }); + const request: ListBilledUsageRequest = { + attributionId, + fromDate: startDateOfBillMonth, + toDate: endDateOfBillMonth, + perPage: 50, + page, + }; + try { + const page = await getGitpodService().server.listBilledUsage(request); + setUsagePage(page); + setTotalCreditsUsed(Math.ceil(page.totalCreditsUsed)); + } catch (error) { + if (error.code === ErrorCodes.PERMISSION_DENIED) { + setErrorMessage("Access to usage details is restricted to team owners."); + } else { + setErrorMessage(`Error: ${error?.message}`); + } + } finally { + setIsLoading(false); + } + }; + const getType = (type: BillableWorkspaceType) => { if (type === "regular") { return "Workspace"; @@ -111,12 +117,6 @@ function TeamUsage() { return inMinutes + " min"; }; - const calculateTotalUsage = () => { - let totalCredits = 0; - billedUsage.forEach((session) => (totalCredits += session.credits)); - return totalCredits.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - }; - const handleMonthClick = (start: any, end: any) => { setStartDateOfBillMonth(start); setEndDateOfBillMonth(end); @@ -155,10 +155,7 @@ function TeamUsage() { return new Date(time).toLocaleDateString(undefined, options).replace("at ", ""); }; - const lastResultOnCurrentPage = currentPage * resultsPerPage; - const firstResultOnCurrentPage = lastResultOnCurrentPage - resultsPerPage; - const totalNumberOfPages = Math.ceil(billedUsage.length / resultsPerPage); - const currentPaginatedResults = billedUsage.slice(firstResultOnCurrentPage, lastResultOnCurrentPage); + const currentPaginatedResults = usagePage?.sessions ?? []; return ( <> @@ -181,16 +178,18 @@ function TeamUsage() {
    Previous Months
    {getBillingHistory()} -
    -
    Total usage
    -
    - - {calculateTotalUsage()} Credits + {!isLoading && ( +
    +
    Total usage
    +
    + + {totalCreditsUsed} Credits +
    -
    + )}
    - {!isLoading && billedUsage.length === 0 && !errorMessage && ( + {!isLoading && usagePage === undefined && !errorMessage && (

    No sessions found.

    @@ -215,7 +214,7 @@ function TeamUsage() {

    )} - {billedUsage.length > 0 && !isLoading && ( + {!isLoading && currentPaginatedResults.length > 0 && (
    - setIsStartedTimeDescending(!isStartedTimeDescending)} - > - Timestamp - - + Timestamp {currentPaginatedResults && @@ -310,12 +299,11 @@ function TeamUsage() { ); })} - {billedUsage.length > resultsPerPage && ( + {usagePage && usagePage.totalPages > 1 && ( loadPage(page)} + totalNumberOfPages={usagePage.totalPages} /> )}
    diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index b5535b54b5cdae..f46c49b99b23f7 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -61,7 +61,7 @@ import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./ import { IDEServer } from "./ide-protocol"; import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol"; import { Currency } from "./plans"; -import { BillableSession, BillableSessionRequest } from "./usage"; +import { ListBilledUsageResponse, ListBilledUsageRequest } from "./usage"; import { SupportedWorkspaceClass } from "./workspace-class"; import { BillingMode } from "./billing-mode"; @@ -297,7 +297,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getSpendingLimitForTeam(teamId: string): Promise; setSpendingLimitForTeam(teamId: string, spendingLimit: number): Promise; - listBilledUsage(req: BillableSessionRequest): Promise; + listBilledUsage(req: ListBilledUsageRequest): Promise; setUsageAttribution(usageAttribution: string): Promise; diff --git a/components/gitpod-protocol/src/usage.ts b/components/gitpod-protocol/src/usage.ts index 78d69a87035e6f..079b5aea0e913b 100644 --- a/components/gitpod-protocol/src/usage.ts +++ b/components/gitpod-protocol/src/usage.ts @@ -40,16 +40,24 @@ export interface ExtendedBillableSession extends BillableSession { user?: Pick; } -export interface BillableSessionRequest { +/** + * This is a paginated request + */ +export interface ListBilledUsageRequest { attributionId: string; - startedTimeOrder: SortOrder; - from?: number; - to?: number; + fromDate?: number; + toDate?: number; + perPage: number; + page: number; } -export type BillableWorkspaceType = WorkspaceType; - -export enum SortOrder { - Descending = 0, - Ascending = 1, +export interface ListBilledUsageResponse { + sessions: ExtendedBillableSession[]; + totalCreditsUsed: number; + totalPages: number; + totalSessions: number; + perPage: number; + page: number; } + +export type BillableWorkspaceType = WorkspaceType; diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 301bb7ba413899..53c56af4b53018 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -71,7 +71,8 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor import { EligibilityService } from "../user/eligibility-service"; import { AccountStatementProvider } from "../user/account-statement-provider"; import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol"; -import { ExtendedBillableSession, BillableSessionRequest } from "@gitpod/gitpod-protocol/lib/usage"; +import { ListBilledUsageRequest, ListBilledUsageResponse } from "@gitpod/gitpod-protocol/lib/usage"; +import { ListBilledUsageRequest as ListBilledUsage } from "@gitpod/usage-api/lib/usage/v1/usage_pb"; import { AssigneeIdentityIdentifier, TeamSubscription, @@ -2149,8 +2150,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return result; } - async listBilledUsage(ctx: TraceContext, req: BillableSessionRequest): Promise { - const { attributionId, startedTimeOrder, from, to } = req; + async listBilledUsage(ctx: TraceContext, req: ListBilledUsageRequest): Promise { + const { attributionId, fromDate, toDate, perPage, page } = req; traceAPIParams(ctx, { attributionId }); let timestampFrom; let timestampTo; @@ -2158,39 +2159,52 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.guardCostCenterAccess(ctx, user.id, attributionId, "get"); - if (from) { - timestampFrom = Timestamp.fromDate(new Date(from)); + if (fromDate) { + timestampFrom = Timestamp.fromDate(new Date(fromDate)); } - if (to) { - timestampTo = Timestamp.fromDate(new Date(to)); + if (toDate) { + timestampTo = Timestamp.fromDate(new Date(toDate)); } const usageClient = this.usageServiceClientProvider.getDefault(); const response = await usageClient.listBilledUsage( ctx, attributionId, - startedTimeOrder as number, + ListBilledUsage.Ordering.ORDERING_DESCENDING, + perPage, + page, timestampFrom, timestampTo, ); - const sessions = response.getSessionsList().map((s) => UsageService.mapBilledSession(s)); - const extendedSessions = await Promise.all( - sessions.map(async (session) => { - const ws = await this.workspaceDb.trace(ctx).findWorkspaceAndInstance(session.workspaceId); - let profile: User.Profile | undefined = undefined; - if (session.workspaceType === "regular" && session.userId) { - const user = await this.userDB.findUserById(session.userId); - if (user) { - profile = User.getProfile(user); + const sessions = await Promise.all( + response + .getSessionsList() + .map((s) => UsageService.mapBilledSession(s)) + .map(async (session) => { + const ws = await this.workspaceDb.trace(ctx).findWorkspaceAndInstance(session.workspaceId); + let profile: User.Profile | undefined = undefined; + if (session.workspaceType === "regular" && session.userId) { + // TODO add caching to void repeated loading of same profile details here + const user = await this.userDB.findUserById(session.userId); + if (user) { + profile = User.getProfile(user); + } } - } - return { - ...session, - contextURL: ws?.contextURL, - user: profile ? { name: profile.name, avatarURL: profile.avatarURL } : undefined, - }; - }), + return { + ...session, + contextURL: ws?.contextURL, + user: profile, + }; + }), ); - return extendedSessions; + const pagination = response.getPagination(); + return { + sessions, + totalSessions: pagination?.getTotal() || 0, + totalPages: pagination?.getTotalPages() || 0, + page: pagination?.getPage() || 0, + perPage: pagination?.getPerPage() || 0, + totalCreditsUsed: response.getTotalCreditsUsed(), + }; } protected async guardCostCenterAccess( diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 5564a540582d5d..462106eddb109d 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -173,7 +173,7 @@ import { LicenseEvaluator } from "@gitpod/licensor/lib"; import { Feature } from "@gitpod/licensor/lib/api"; import { Currency } from "@gitpod/gitpod-protocol/lib/plans"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; -import { BillableSession, BillableSessionRequest } from "@gitpod/gitpod-protocol/lib/usage"; +import { ListBilledUsageRequest, ListBilledUsageResponse } from "@gitpod/gitpod-protocol/lib/usage"; import { WorkspaceClusterImagebuilderClientProvider } from "./workspace-cluster-imagebuilder-client-provider"; import { VerificationService } from "../auth/verification-service"; import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; @@ -3227,7 +3227,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } - async listBilledUsage(ctx: TraceContext, req: BillableSessionRequest): Promise { + async listBilledUsage(ctx: TraceContext, req: ListBilledUsageRequest): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } diff --git a/components/usage-api/go/v1/usage.pb.go b/components/usage-api/go/v1/usage.pb.go index 8b4b640bf7d4ef..4d784645719c9e 100644 --- a/components/usage-api/go/v1/usage.pb.go +++ b/components/usage-api/go/v1/usage.pb.go @@ -82,8 +82,9 @@ type ListBilledUsageRequest struct { From *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"` // to specifies the end time range for this request. // All instances which existed ending at to will be returned. - To *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=to,proto3" json:"to,omitempty"` - Order ListBilledUsageRequest_Ordering `protobuf:"varint,4,opt,name=order,proto3,enum=usage.v1.ListBilledUsageRequest_Ordering" json:"order,omitempty"` + To *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=to,proto3" json:"to,omitempty"` + Order ListBilledUsageRequest_Ordering `protobuf:"varint,4,opt,name=order,proto3,enum=usage.v1.ListBilledUsageRequest_Ordering" json:"order,omitempty"` + Pagination *PaginatedRequest `protobuf:"bytes,5,opt,name=pagination,proto3" json:"pagination,omitempty"` } func (x *ListBilledUsageRequest) Reset() { @@ -146,18 +147,82 @@ func (x *ListBilledUsageRequest) GetOrder() ListBilledUsageRequest_Ordering { return ListBilledUsageRequest_ORDERING_DESCENDING } +func (x *ListBilledUsageRequest) GetPagination() *PaginatedRequest { + if x != nil { + return x.Pagination + } + return nil +} + +type PaginatedRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PerPage int64 `protobuf:"varint,1,opt,name=per_page,json=perPage,proto3" json:"per_page,omitempty"` + Page int64 `protobuf:"varint,2,opt,name=page,proto3" json:"page,omitempty"` +} + +func (x *PaginatedRequest) Reset() { + *x = PaginatedRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_usage_v1_usage_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PaginatedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaginatedRequest) ProtoMessage() {} + +func (x *PaginatedRequest) ProtoReflect() protoreflect.Message { + mi := &file_usage_v1_usage_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaginatedRequest.ProtoReflect.Descriptor instead. +func (*PaginatedRequest) Descriptor() ([]byte, []int) { + return file_usage_v1_usage_proto_rawDescGZIP(), []int{1} +} + +func (x *PaginatedRequest) GetPerPage() int64 { + if x != nil { + return x.PerPage + } + return 0 +} + +func (x *PaginatedRequest) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + type ListBilledUsageResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Sessions []*BilledSession `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` + Sessions []*BilledSession `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` + TotalCreditsUsed float64 `protobuf:"fixed64,2,opt,name=total_credits_used,json=totalCreditsUsed,proto3" json:"total_credits_used,omitempty"` + Pagination *PaginatedResponse `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` } func (x *ListBilledUsageResponse) Reset() { *x = ListBilledUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[1] + mi := &file_usage_v1_usage_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -170,7 +235,7 @@ func (x *ListBilledUsageResponse) String() string { func (*ListBilledUsageResponse) ProtoMessage() {} func (x *ListBilledUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[1] + mi := &file_usage_v1_usage_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -183,7 +248,7 @@ func (x *ListBilledUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBilledUsageResponse.ProtoReflect.Descriptor instead. func (*ListBilledUsageResponse) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{1} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{2} } func (x *ListBilledUsageResponse) GetSessions() []*BilledSession { @@ -193,6 +258,91 @@ func (x *ListBilledUsageResponse) GetSessions() []*BilledSession { return nil } +func (x *ListBilledUsageResponse) GetTotalCreditsUsed() float64 { + if x != nil { + return x.TotalCreditsUsed + } + return 0 +} + +func (x *ListBilledUsageResponse) GetPagination() *PaginatedResponse { + if x != nil { + return x.Pagination + } + return nil +} + +type PaginatedResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PerPage int64 `protobuf:"varint,2,opt,name=per_page,json=perPage,proto3" json:"per_page,omitempty"` + TotalPages int64 `protobuf:"varint,3,opt,name=total_pages,json=totalPages,proto3" json:"total_pages,omitempty"` + Total int64 `protobuf:"varint,4,opt,name=total,proto3" json:"total,omitempty"` + Page int64 `protobuf:"varint,5,opt,name=page,proto3" json:"page,omitempty"` +} + +func (x *PaginatedResponse) Reset() { + *x = PaginatedResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_usage_v1_usage_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PaginatedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaginatedResponse) ProtoMessage() {} + +func (x *PaginatedResponse) ProtoReflect() protoreflect.Message { + mi := &file_usage_v1_usage_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaginatedResponse.ProtoReflect.Descriptor instead. +func (*PaginatedResponse) Descriptor() ([]byte, []int) { + return file_usage_v1_usage_proto_rawDescGZIP(), []int{3} +} + +func (x *PaginatedResponse) GetPerPage() int64 { + if x != nil { + return x.PerPage + } + return 0 +} + +func (x *PaginatedResponse) GetTotalPages() int64 { + if x != nil { + return x.TotalPages + } + return 0 +} + +func (x *PaginatedResponse) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *PaginatedResponse) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + type BilledSession struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -216,7 +366,7 @@ type BilledSession struct { func (x *BilledSession) Reset() { *x = BilledSession{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[2] + mi := &file_usage_v1_usage_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -229,7 +379,7 @@ func (x *BilledSession) String() string { func (*BilledSession) ProtoMessage() {} func (x *BilledSession) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[2] + mi := &file_usage_v1_usage_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -242,7 +392,7 @@ func (x *BilledSession) ProtoReflect() protoreflect.Message { // Deprecated: Use BilledSession.ProtoReflect.Descriptor instead. func (*BilledSession) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{2} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{4} } func (x *BilledSession) GetAttributionId() string { @@ -342,7 +492,7 @@ type ReconcileUsageRequest struct { func (x *ReconcileUsageRequest) Reset() { *x = ReconcileUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[3] + mi := &file_usage_v1_usage_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +505,7 @@ func (x *ReconcileUsageRequest) String() string { func (*ReconcileUsageRequest) ProtoMessage() {} func (x *ReconcileUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[3] + mi := &file_usage_v1_usage_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +518,7 @@ func (x *ReconcileUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReconcileUsageRequest.ProtoReflect.Descriptor instead. func (*ReconcileUsageRequest) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{3} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{5} } func (x *ReconcileUsageRequest) GetStartTime() *timestamppb.Timestamp { @@ -399,7 +549,7 @@ type ReconcileUsageResponse struct { func (x *ReconcileUsageResponse) Reset() { *x = ReconcileUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[4] + mi := &file_usage_v1_usage_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -412,7 +562,7 @@ func (x *ReconcileUsageResponse) String() string { func (*ReconcileUsageResponse) ProtoMessage() {} func (x *ReconcileUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[4] + mi := &file_usage_v1_usage_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -425,7 +575,7 @@ func (x *ReconcileUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReconcileUsageResponse.ProtoReflect.Descriptor instead. func (*ReconcileUsageResponse) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{4} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{6} } // Deprecated: Do not use. @@ -454,7 +604,7 @@ type GetCostCenterRequest struct { func (x *GetCostCenterRequest) Reset() { *x = GetCostCenterRequest{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[5] + mi := &file_usage_v1_usage_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -467,7 +617,7 @@ func (x *GetCostCenterRequest) String() string { func (*GetCostCenterRequest) ProtoMessage() {} func (x *GetCostCenterRequest) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[5] + mi := &file_usage_v1_usage_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -480,7 +630,7 @@ func (x *GetCostCenterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCostCenterRequest.ProtoReflect.Descriptor instead. func (*GetCostCenterRequest) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{5} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{7} } func (x *GetCostCenterRequest) GetAttributionId() string { @@ -501,7 +651,7 @@ type GetCostCenterResponse struct { func (x *GetCostCenterResponse) Reset() { *x = GetCostCenterResponse{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[6] + mi := &file_usage_v1_usage_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -514,7 +664,7 @@ func (x *GetCostCenterResponse) String() string { func (*GetCostCenterResponse) ProtoMessage() {} func (x *GetCostCenterResponse) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[6] + mi := &file_usage_v1_usage_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -527,7 +677,7 @@ func (x *GetCostCenterResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCostCenterResponse.ProtoReflect.Descriptor instead. func (*GetCostCenterResponse) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{6} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{8} } func (x *GetCostCenterResponse) GetCostCenter() *CostCenter { @@ -549,7 +699,7 @@ type CostCenter struct { func (x *CostCenter) Reset() { *x = CostCenter{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[7] + mi := &file_usage_v1_usage_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -562,7 +712,7 @@ func (x *CostCenter) String() string { func (*CostCenter) ProtoMessage() {} func (x *CostCenter) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[7] + mi := &file_usage_v1_usage_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -575,7 +725,7 @@ func (x *CostCenter) ProtoReflect() protoreflect.Message { // Deprecated: Use CostCenter.ProtoReflect.Descriptor instead. func (*CostCenter) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{7} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{9} } func (x *CostCenter) GetAttributionId() string { @@ -599,7 +749,7 @@ var file_usage_v1_usage_proto_rawDesc = []byte{ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x99, 0x02, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, + 0x6f, 0x22, 0xd5, 0x02, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, @@ -613,96 +763,119 @@ var file_usage_v1_usage_proto_rawDesc = []byte{ 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, - 0x22, 0x3b, 0x0a, 0x08, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x13, - 0x4f, 0x52, 0x44, 0x45, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x45, 0x53, 0x43, 0x45, 0x4e, 0x44, - 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x49, 0x4e, - 0x47, 0x5f, 0x41, 0x53, 0x43, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x22, 0x4e, 0x0a, + 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3b, 0x0a, 0x08, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x13, 0x4f, 0x52, 0x44, 0x45, + 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x44, 0x45, 0x53, 0x43, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, + 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x41, 0x53, + 0x43, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x22, 0x41, 0x0a, 0x10, 0x50, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, + 0x08, 0x70, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x70, 0x65, 0x72, 0x50, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x22, 0xb9, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xda, 0x03, - 0x0a, 0x0d, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x17, 0x0a, 0x07, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x74, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, - 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x31, 0x0a, - 0x12, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x5f, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, - 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x11, 0x63, - 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x44, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x01, 0x52, 0x07, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x15, 0x52, - 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, - 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x6e, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, - 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x69, - 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x02, 0x18, 0x01, 0x52, - 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x22, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, - 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, - 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, - 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, - 0x0a, 0x0b, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x73, 0x74, 0x43, - 0x65, 0x6e, 0x74, 0x65, 0x72, 0x22, 0x5a, 0x0a, 0x0a, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, - 0x74, 0x65, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x70, - 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0d, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x6d, 0x69, - 0x74, 0x32, 0x93, 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x0e, - 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, - 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, - 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, - 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, - 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2c, 0x0a, + 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x5f, 0x75, + 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x3b, 0x0a, 0x0a, 0x70, + 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x79, 0x0a, 0x11, 0x50, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x70, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x70, 0x65, 0x72, 0x50, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x61, 0x67, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, + 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x70, + 0x61, 0x67, 0x65, 0x22, 0xda, 0x03, 0x0a, 0x0d, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, + 0x73, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, + 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x5f, 0x64, + 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x02, 0x18, 0x01, 0x52, 0x11, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x44, 0x65, 0x70, 0x72, + 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, + 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x01, 0x52, 0x07, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, + 0x22, 0x89, 0x01, 0x0a, 0x15, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x6e, 0x0a, 0x16, + 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x22, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, - 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2d, 0x61, 0x70, 0x69, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x15, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, + 0x0a, 0x63, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x22, 0x5a, 0x0a, 0x0a, 0x43, + 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x32, 0x93, 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, + 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x2e, 0x75, 0x73, + 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, + 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x69, 0x6c, + 0x6c, 0x65, 0x64, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x55, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x0d, 0x47, 0x65, 0x74, + 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x61, + 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x61, + 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2a, 0x5a, + 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, + 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x75, 0x73, 0x61, + 0x67, 0x65, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -718,41 +891,45 @@ func file_usage_v1_usage_proto_rawDescGZIP() []byte { } var file_usage_v1_usage_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_usage_v1_usage_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_usage_v1_usage_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_usage_v1_usage_proto_goTypes = []interface{}{ (ListBilledUsageRequest_Ordering)(0), // 0: usage.v1.ListBilledUsageRequest.Ordering (*ListBilledUsageRequest)(nil), // 1: usage.v1.ListBilledUsageRequest - (*ListBilledUsageResponse)(nil), // 2: usage.v1.ListBilledUsageResponse - (*BilledSession)(nil), // 3: usage.v1.BilledSession - (*ReconcileUsageRequest)(nil), // 4: usage.v1.ReconcileUsageRequest - (*ReconcileUsageResponse)(nil), // 5: usage.v1.ReconcileUsageResponse - (*GetCostCenterRequest)(nil), // 6: usage.v1.GetCostCenterRequest - (*GetCostCenterResponse)(nil), // 7: usage.v1.GetCostCenterResponse - (*CostCenter)(nil), // 8: usage.v1.CostCenter - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp + (*PaginatedRequest)(nil), // 2: usage.v1.PaginatedRequest + (*ListBilledUsageResponse)(nil), // 3: usage.v1.ListBilledUsageResponse + (*PaginatedResponse)(nil), // 4: usage.v1.PaginatedResponse + (*BilledSession)(nil), // 5: usage.v1.BilledSession + (*ReconcileUsageRequest)(nil), // 6: usage.v1.ReconcileUsageRequest + (*ReconcileUsageResponse)(nil), // 7: usage.v1.ReconcileUsageResponse + (*GetCostCenterRequest)(nil), // 8: usage.v1.GetCostCenterRequest + (*GetCostCenterResponse)(nil), // 9: usage.v1.GetCostCenterResponse + (*CostCenter)(nil), // 10: usage.v1.CostCenter + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp } var file_usage_v1_usage_proto_depIdxs = []int32{ - 9, // 0: usage.v1.ListBilledUsageRequest.from:type_name -> google.protobuf.Timestamp - 9, // 1: usage.v1.ListBilledUsageRequest.to:type_name -> google.protobuf.Timestamp + 11, // 0: usage.v1.ListBilledUsageRequest.from:type_name -> google.protobuf.Timestamp + 11, // 1: usage.v1.ListBilledUsageRequest.to:type_name -> google.protobuf.Timestamp 0, // 2: usage.v1.ListBilledUsageRequest.order:type_name -> usage.v1.ListBilledUsageRequest.Ordering - 3, // 3: usage.v1.ListBilledUsageResponse.sessions:type_name -> usage.v1.BilledSession - 9, // 4: usage.v1.BilledSession.start_time:type_name -> google.protobuf.Timestamp - 9, // 5: usage.v1.BilledSession.end_time:type_name -> google.protobuf.Timestamp - 9, // 6: usage.v1.ReconcileUsageRequest.start_time:type_name -> google.protobuf.Timestamp - 9, // 7: usage.v1.ReconcileUsageRequest.end_time:type_name -> google.protobuf.Timestamp - 3, // 8: usage.v1.ReconcileUsageResponse.sessions:type_name -> usage.v1.BilledSession - 8, // 9: usage.v1.GetCostCenterResponse.cost_center:type_name -> usage.v1.CostCenter - 1, // 10: usage.v1.UsageService.ListBilledUsage:input_type -> usage.v1.ListBilledUsageRequest - 4, // 11: usage.v1.UsageService.ReconcileUsage:input_type -> usage.v1.ReconcileUsageRequest - 6, // 12: usage.v1.UsageService.GetCostCenter:input_type -> usage.v1.GetCostCenterRequest - 2, // 13: usage.v1.UsageService.ListBilledUsage:output_type -> usage.v1.ListBilledUsageResponse - 5, // 14: usage.v1.UsageService.ReconcileUsage:output_type -> usage.v1.ReconcileUsageResponse - 7, // 15: usage.v1.UsageService.GetCostCenter:output_type -> usage.v1.GetCostCenterResponse - 13, // [13:16] is the sub-list for method output_type - 10, // [10:13] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 2, // 3: usage.v1.ListBilledUsageRequest.pagination:type_name -> usage.v1.PaginatedRequest + 5, // 4: usage.v1.ListBilledUsageResponse.sessions:type_name -> usage.v1.BilledSession + 4, // 5: usage.v1.ListBilledUsageResponse.pagination:type_name -> usage.v1.PaginatedResponse + 11, // 6: usage.v1.BilledSession.start_time:type_name -> google.protobuf.Timestamp + 11, // 7: usage.v1.BilledSession.end_time:type_name -> google.protobuf.Timestamp + 11, // 8: usage.v1.ReconcileUsageRequest.start_time:type_name -> google.protobuf.Timestamp + 11, // 9: usage.v1.ReconcileUsageRequest.end_time:type_name -> google.protobuf.Timestamp + 5, // 10: usage.v1.ReconcileUsageResponse.sessions:type_name -> usage.v1.BilledSession + 10, // 11: usage.v1.GetCostCenterResponse.cost_center:type_name -> usage.v1.CostCenter + 1, // 12: usage.v1.UsageService.ListBilledUsage:input_type -> usage.v1.ListBilledUsageRequest + 6, // 13: usage.v1.UsageService.ReconcileUsage:input_type -> usage.v1.ReconcileUsageRequest + 8, // 14: usage.v1.UsageService.GetCostCenter:input_type -> usage.v1.GetCostCenterRequest + 3, // 15: usage.v1.UsageService.ListBilledUsage:output_type -> usage.v1.ListBilledUsageResponse + 7, // 16: usage.v1.UsageService.ReconcileUsage:output_type -> usage.v1.ReconcileUsageResponse + 9, // 17: usage.v1.UsageService.GetCostCenter:output_type -> usage.v1.GetCostCenterResponse + 15, // [15:18] is the sub-list for method output_type + 12, // [12:15] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_usage_v1_usage_proto_init() } @@ -774,7 +951,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListBilledUsageResponse); i { + switch v := v.(*PaginatedRequest); i { case 0: return &v.state case 1: @@ -786,7 +963,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BilledSession); i { + switch v := v.(*ListBilledUsageResponse); i { case 0: return &v.state case 1: @@ -798,7 +975,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReconcileUsageRequest); i { + switch v := v.(*PaginatedResponse); i { case 0: return &v.state case 1: @@ -810,7 +987,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReconcileUsageResponse); i { + switch v := v.(*BilledSession); i { case 0: return &v.state case 1: @@ -822,7 +999,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetCostCenterRequest); i { + switch v := v.(*ReconcileUsageRequest); i { case 0: return &v.state case 1: @@ -834,7 +1011,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetCostCenterResponse); i { + switch v := v.(*ReconcileUsageResponse); i { case 0: return &v.state case 1: @@ -846,6 +1023,30 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCostCenterRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_usage_v1_usage_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCostCenterResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_usage_v1_usage_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CostCenter); i { case 0: return &v.state @@ -864,7 +1065,7 @@ func file_usage_v1_usage_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_usage_v1_usage_proto_rawDesc, NumEnums: 1, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/components/usage-api/typescript/src/usage/v1/sugar.ts b/components/usage-api/typescript/src/usage/v1/sugar.ts index d3fc76f280a78b..4baa9216bd6b20 100644 --- a/components/usage-api/typescript/src/usage/v1/sugar.ts +++ b/components/usage-api/typescript/src/usage/v1/sugar.ts @@ -9,7 +9,7 @@ import { BillingServiceClient } from "./billing_grpc_pb"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import * as opentracing from "opentracing"; import { Metadata } from "@grpc/grpc-js"; -import { BilledSession, ListBilledUsageRequest, ListBilledUsageResponse } from "./usage_pb"; +import { BilledSession, ListBilledUsageRequest, ListBilledUsageResponse, PaginatedRequest } from "./usage_pb"; import { GetUpcomingInvoiceRequest, GetUpcomingInvoiceResponse, @@ -153,14 +153,21 @@ export class PromisifiedUsageServiceClient { _ctx: TraceContext, attributionId: string, order: ListBilledUsageRequest.Ordering, + perPage: number, + page: number, from?: Timestamp, to?: Timestamp, ): Promise { const ctx = TraceContext.childContext(`/usage-service/listBilledUsage`, _ctx); try { + const pagination = new PaginatedRequest(); + pagination.setPerPage(perPage); + pagination.setPage(page); + const req = new ListBilledUsageRequest(); req.setAttributionId(attributionId); + req.setPagination(pagination); req.setFrom(from); req.setTo(to); req.setOrder(order); diff --git a/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts b/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts index 45aa384db4e75e..28dd34ada8db40 100644 --- a/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts +++ b/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts @@ -29,6 +29,11 @@ export class ListBilledUsageRequest extends jspb.Message { getOrder(): ListBilledUsageRequest.Ordering; setOrder(value: ListBilledUsageRequest.Ordering): ListBilledUsageRequest; + hasPagination(): boolean; + clearPagination(): void; + getPagination(): PaginatedRequest | undefined; + setPagination(value?: PaginatedRequest): ListBilledUsageRequest; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ListBilledUsageRequest.AsObject; static toObject(includeInstance: boolean, msg: ListBilledUsageRequest): ListBilledUsageRequest.AsObject; @@ -45,6 +50,7 @@ export namespace ListBilledUsageRequest { from?: google_protobuf_timestamp_pb.Timestamp.AsObject, to?: google_protobuf_timestamp_pb.Timestamp.AsObject, order: ListBilledUsageRequest.Ordering, + pagination?: PaginatedRequest.AsObject, } export enum Ordering { @@ -54,11 +60,41 @@ export namespace ListBilledUsageRequest { } +export class PaginatedRequest extends jspb.Message { + getPerPage(): number; + setPerPage(value: number): PaginatedRequest; + getPage(): number; + setPage(value: number): PaginatedRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): PaginatedRequest.AsObject; + static toObject(includeInstance: boolean, msg: PaginatedRequest): PaginatedRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: PaginatedRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): PaginatedRequest; + static deserializeBinaryFromReader(message: PaginatedRequest, reader: jspb.BinaryReader): PaginatedRequest; +} + +export namespace PaginatedRequest { + export type AsObject = { + perPage: number, + page: number, + } +} + export class ListBilledUsageResponse extends jspb.Message { clearSessionsList(): void; getSessionsList(): Array; setSessionsList(value: Array): ListBilledUsageResponse; addSessions(value?: BilledSession, index?: number): BilledSession; + getTotalCreditsUsed(): number; + setTotalCreditsUsed(value: number): ListBilledUsageResponse; + + hasPagination(): boolean; + clearPagination(): void; + getPagination(): PaginatedResponse | undefined; + setPagination(value?: PaginatedResponse): ListBilledUsageResponse; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ListBilledUsageResponse.AsObject; @@ -73,6 +109,37 @@ export class ListBilledUsageResponse extends jspb.Message { export namespace ListBilledUsageResponse { export type AsObject = { sessionsList: Array, + totalCreditsUsed: number, + pagination?: PaginatedResponse.AsObject, + } +} + +export class PaginatedResponse extends jspb.Message { + getPerPage(): number; + setPerPage(value: number): PaginatedResponse; + getTotalPages(): number; + setTotalPages(value: number): PaginatedResponse; + getTotal(): number; + setTotal(value: number): PaginatedResponse; + getPage(): number; + setPage(value: number): PaginatedResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): PaginatedResponse.AsObject; + static toObject(includeInstance: boolean, msg: PaginatedResponse): PaginatedResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: PaginatedResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): PaginatedResponse; + static deserializeBinaryFromReader(message: PaginatedResponse, reader: jspb.BinaryReader): PaginatedResponse; +} + +export namespace PaginatedResponse { + export type AsObject = { + perPage: number, + totalPages: number, + total: number, + page: number, } } diff --git a/components/usage-api/typescript/src/usage/v1/usage_pb.js b/components/usage-api/typescript/src/usage/v1/usage_pb.js index 6421d812413ebb..066105cc0e8bc1 100644 --- a/components/usage-api/typescript/src/usage/v1/usage_pb.js +++ b/components/usage-api/typescript/src/usage/v1/usage_pb.js @@ -30,6 +30,8 @@ goog.exportSymbol('proto.usage.v1.GetCostCenterResponse', null, global); goog.exportSymbol('proto.usage.v1.ListBilledUsageRequest', null, global); goog.exportSymbol('proto.usage.v1.ListBilledUsageRequest.Ordering', null, global); goog.exportSymbol('proto.usage.v1.ListBilledUsageResponse', null, global); +goog.exportSymbol('proto.usage.v1.PaginatedRequest', null, global); +goog.exportSymbol('proto.usage.v1.PaginatedResponse', null, global); goog.exportSymbol('proto.usage.v1.ReconcileUsageRequest', null, global); goog.exportSymbol('proto.usage.v1.ReconcileUsageResponse', null, global); /** @@ -53,6 +55,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.usage.v1.ListBilledUsageRequest.displayName = 'proto.usage.v1.ListBilledUsageRequest'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.usage.v1.PaginatedRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.usage.v1.PaginatedRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.usage.v1.PaginatedRequest.displayName = 'proto.usage.v1.PaginatedRequest'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -74,6 +97,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.usage.v1.ListBilledUsageResponse.displayName = 'proto.usage.v1.ListBilledUsageResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.usage.v1.PaginatedResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.usage.v1.PaginatedResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.usage.v1.PaginatedResponse.displayName = 'proto.usage.v1.PaginatedResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -235,7 +279,8 @@ proto.usage.v1.ListBilledUsageRequest.toObject = function(includeInstance, msg) attributionId: jspb.Message.getFieldWithDefault(msg, 1, ""), from: (f = msg.getFrom()) && google_protobuf_timestamp_pb.Timestamp.toObject(includeInstance, f), to: (f = msg.getTo()) && google_protobuf_timestamp_pb.Timestamp.toObject(includeInstance, f), - order: jspb.Message.getFieldWithDefault(msg, 4, 0) + order: jspb.Message.getFieldWithDefault(msg, 4, 0), + pagination: (f = msg.getPagination()) && proto.usage.v1.PaginatedRequest.toObject(includeInstance, f) }; if (includeInstance) { @@ -290,6 +335,11 @@ proto.usage.v1.ListBilledUsageRequest.deserializeBinaryFromReader = function(msg var value = /** @type {!proto.usage.v1.ListBilledUsageRequest.Ordering} */ (reader.readEnum()); msg.setOrder(value); break; + case 5: + var value = new proto.usage.v1.PaginatedRequest; + reader.readMessage(value,proto.usage.v1.PaginatedRequest.deserializeBinaryFromReader); + msg.setPagination(value); + break; default: reader.skipField(); break; @@ -349,6 +399,14 @@ proto.usage.v1.ListBilledUsageRequest.serializeBinaryToWriter = function(message f ); } + f = message.getPagination(); + if (f != null) { + writer.writeMessage( + 5, + f, + proto.usage.v1.PaginatedRequest.serializeBinaryToWriter + ); + } }; @@ -470,6 +528,203 @@ proto.usage.v1.ListBilledUsageRequest.prototype.setOrder = function(value) { }; +/** + * optional PaginatedRequest pagination = 5; + * @return {?proto.usage.v1.PaginatedRequest} + */ +proto.usage.v1.ListBilledUsageRequest.prototype.getPagination = function() { + return /** @type{?proto.usage.v1.PaginatedRequest} */ ( + jspb.Message.getWrapperField(this, proto.usage.v1.PaginatedRequest, 5)); +}; + + +/** + * @param {?proto.usage.v1.PaginatedRequest|undefined} value + * @return {!proto.usage.v1.ListBilledUsageRequest} returns this +*/ +proto.usage.v1.ListBilledUsageRequest.prototype.setPagination = function(value) { + return jspb.Message.setWrapperField(this, 5, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.usage.v1.ListBilledUsageRequest} returns this + */ +proto.usage.v1.ListBilledUsageRequest.prototype.clearPagination = function() { + return this.setPagination(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.usage.v1.ListBilledUsageRequest.prototype.hasPagination = function() { + return jspb.Message.getField(this, 5) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.usage.v1.PaginatedRequest.prototype.toObject = function(opt_includeInstance) { + return proto.usage.v1.PaginatedRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.usage.v1.PaginatedRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.PaginatedRequest.toObject = function(includeInstance, msg) { + var f, obj = { + perPage: jspb.Message.getFieldWithDefault(msg, 1, 0), + page: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.usage.v1.PaginatedRequest} + */ +proto.usage.v1.PaginatedRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.usage.v1.PaginatedRequest; + return proto.usage.v1.PaginatedRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.usage.v1.PaginatedRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.usage.v1.PaginatedRequest} + */ +proto.usage.v1.PaginatedRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt64()); + msg.setPerPage(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setPage(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.usage.v1.PaginatedRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.usage.v1.PaginatedRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.usage.v1.PaginatedRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.PaginatedRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getPerPage(); + if (f !== 0) { + writer.writeInt64( + 1, + f + ); + } + f = message.getPage(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } +}; + + +/** + * optional int64 per_page = 1; + * @return {number} + */ +proto.usage.v1.PaginatedRequest.prototype.getPerPage = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.PaginatedRequest} returns this + */ +proto.usage.v1.PaginatedRequest.prototype.setPerPage = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional int64 page = 2; + * @return {number} + */ +proto.usage.v1.PaginatedRequest.prototype.getPage = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.PaginatedRequest} returns this + */ +proto.usage.v1.PaginatedRequest.prototype.setPage = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + /** * List of repeated fields within this message type. @@ -510,7 +765,9 @@ proto.usage.v1.ListBilledUsageResponse.prototype.toObject = function(opt_include proto.usage.v1.ListBilledUsageResponse.toObject = function(includeInstance, msg) { var f, obj = { sessionsList: jspb.Message.toObjectList(msg.getSessionsList(), - proto.usage.v1.BilledSession.toObject, includeInstance) + proto.usage.v1.BilledSession.toObject, includeInstance), + totalCreditsUsed: jspb.Message.getFloatingPointFieldWithDefault(msg, 2, 0.0), + pagination: (f = msg.getPagination()) && proto.usage.v1.PaginatedResponse.toObject(includeInstance, f) }; if (includeInstance) { @@ -552,6 +809,15 @@ proto.usage.v1.ListBilledUsageResponse.deserializeBinaryFromReader = function(ms reader.readMessage(value,proto.usage.v1.BilledSession.deserializeBinaryFromReader); msg.addSessions(value); break; + case 2: + var value = /** @type {number} */ (reader.readDouble()); + msg.setTotalCreditsUsed(value); + break; + case 3: + var value = new proto.usage.v1.PaginatedResponse; + reader.readMessage(value,proto.usage.v1.PaginatedResponse.deserializeBinaryFromReader); + msg.setPagination(value); + break; default: reader.skipField(); break; @@ -589,6 +855,21 @@ proto.usage.v1.ListBilledUsageResponse.serializeBinaryToWriter = function(messag proto.usage.v1.BilledSession.serializeBinaryToWriter ); } + f = message.getTotalCreditsUsed(); + if (f !== 0.0) { + writer.writeDouble( + 2, + f + ); + } + f = message.getPagination(); + if (f != null) { + writer.writeMessage( + 3, + f, + proto.usage.v1.PaginatedResponse.serializeBinaryToWriter + ); + } }; @@ -630,6 +911,281 @@ proto.usage.v1.ListBilledUsageResponse.prototype.clearSessionsList = function() }; +/** + * optional double total_credits_used = 2; + * @return {number} + */ +proto.usage.v1.ListBilledUsageResponse.prototype.getTotalCreditsUsed = function() { + return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 2, 0.0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.ListBilledUsageResponse} returns this + */ +proto.usage.v1.ListBilledUsageResponse.prototype.setTotalCreditsUsed = function(value) { + return jspb.Message.setProto3FloatField(this, 2, value); +}; + + +/** + * optional PaginatedResponse pagination = 3; + * @return {?proto.usage.v1.PaginatedResponse} + */ +proto.usage.v1.ListBilledUsageResponse.prototype.getPagination = function() { + return /** @type{?proto.usage.v1.PaginatedResponse} */ ( + jspb.Message.getWrapperField(this, proto.usage.v1.PaginatedResponse, 3)); +}; + + +/** + * @param {?proto.usage.v1.PaginatedResponse|undefined} value + * @return {!proto.usage.v1.ListBilledUsageResponse} returns this +*/ +proto.usage.v1.ListBilledUsageResponse.prototype.setPagination = function(value) { + return jspb.Message.setWrapperField(this, 3, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.usage.v1.ListBilledUsageResponse} returns this + */ +proto.usage.v1.ListBilledUsageResponse.prototype.clearPagination = function() { + return this.setPagination(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.usage.v1.ListBilledUsageResponse.prototype.hasPagination = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.usage.v1.PaginatedResponse.prototype.toObject = function(opt_includeInstance) { + return proto.usage.v1.PaginatedResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.usage.v1.PaginatedResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.PaginatedResponse.toObject = function(includeInstance, msg) { + var f, obj = { + perPage: jspb.Message.getFieldWithDefault(msg, 2, 0), + totalPages: jspb.Message.getFieldWithDefault(msg, 3, 0), + total: jspb.Message.getFieldWithDefault(msg, 4, 0), + page: jspb.Message.getFieldWithDefault(msg, 5, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.usage.v1.PaginatedResponse} + */ +proto.usage.v1.PaginatedResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.usage.v1.PaginatedResponse; + return proto.usage.v1.PaginatedResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.usage.v1.PaginatedResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.usage.v1.PaginatedResponse} + */ +proto.usage.v1.PaginatedResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setPerPage(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setTotalPages(value); + break; + case 4: + var value = /** @type {number} */ (reader.readInt64()); + msg.setTotal(value); + break; + case 5: + var value = /** @type {number} */ (reader.readInt64()); + msg.setPage(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.usage.v1.PaginatedResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.usage.v1.PaginatedResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.usage.v1.PaginatedResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.PaginatedResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getPerPage(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } + f = message.getTotalPages(); + if (f !== 0) { + writer.writeInt64( + 3, + f + ); + } + f = message.getTotal(); + if (f !== 0) { + writer.writeInt64( + 4, + f + ); + } + f = message.getPage(); + if (f !== 0) { + writer.writeInt64( + 5, + f + ); + } +}; + + +/** + * optional int64 per_page = 2; + * @return {number} + */ +proto.usage.v1.PaginatedResponse.prototype.getPerPage = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.PaginatedResponse} returns this + */ +proto.usage.v1.PaginatedResponse.prototype.setPerPage = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional int64 total_pages = 3; + * @return {number} + */ +proto.usage.v1.PaginatedResponse.prototype.getTotalPages = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.PaginatedResponse} returns this + */ +proto.usage.v1.PaginatedResponse.prototype.setTotalPages = function(value) { + return jspb.Message.setProto3IntField(this, 3, value); +}; + + +/** + * optional int64 total = 4; + * @return {number} + */ +proto.usage.v1.PaginatedResponse.prototype.getTotal = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.PaginatedResponse} returns this + */ +proto.usage.v1.PaginatedResponse.prototype.setTotal = function(value) { + return jspb.Message.setProto3IntField(this, 4, value); +}; + + +/** + * optional int64 page = 5; + * @return {number} + */ +proto.usage.v1.PaginatedResponse.prototype.getPage = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 5, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.PaginatedResponse} returns this + */ +proto.usage.v1.PaginatedResponse.prototype.setPage = function(value) { + return jspb.Message.setProto3IntField(this, 5, value); +}; + + diff --git a/components/usage-api/usage/v1/usage.proto b/components/usage-api/usage/v1/usage.proto index 55214f0d491339..09e0db9db9908d 100644 --- a/components/usage-api/usage/v1/usage.proto +++ b/components/usage-api/usage/v1/usage.proto @@ -34,10 +34,26 @@ message ListBilledUsageRequest { } Ordering order = 4; + + PaginatedRequest pagination = 5; +} + +message PaginatedRequest { + int64 per_page = 1; + int64 page = 2; } message ListBilledUsageResponse { repeated BilledSession sessions = 1; + double total_credits_used = 2; + PaginatedResponse pagination = 3; +} + +message PaginatedResponse { + int64 per_page = 2; + int64 total_pages = 3; + int64 total = 4; + int64 page = 5; } message BilledSession { diff --git a/components/usage/pkg/apiv1/usage.go b/components/usage/pkg/apiv1/usage.go index f0f37897df55d4..402e2289f76f1b 100644 --- a/components/usage/pkg/apiv1/usage.go +++ b/components/usage/pkg/apiv1/usage.go @@ -9,6 +9,7 @@ import ( "database/sql" "errors" "fmt" + "math" "time" "github.com/gitpod-io/gitpod/common-go/log" @@ -54,18 +55,26 @@ func (s *UsageService) ListBilledUsage(ctx context.Context, in *v1.ListBilledUsa return nil, status.Errorf(codes.InvalidArgument, "Maximum range exceeded. Range specified can be at most %s", maxQuerySize.String()) } - var order db.Order - switch in.Order { - case v1.ListBilledUsageRequest_ORDERING_ASCENDING: + var order db.Order = db.DescendingOrder + if in.Order == v1.ListBilledUsageRequest_ORDERING_ASCENDING { order = db.AscendingOrder - default: - order = db.DescendingOrder } - usageRecords, err := db.ListUsage(ctx, s.conn, db.AttributionID(in.GetAttributionId()), from, to, order) + var limit int64 = 1000 + var page int64 = 0 + var offset int64 = 0 + if in.Pagination != nil { + limit = in.Pagination.PerPage + page = in.Pagination.Page + offset = limit * (int64(math.Max(0, float64(page-1)))) + } + + listUsageResult, err := db.ListUsage(ctx, s.conn, db.AttributionID(in.GetAttributionId()), from, to, order, offset, limit) if err != nil { log.Log. WithField("attribution_id", in.AttributionId). + WithField("perPage", limit). + WithField("page", page). WithField("from", from). WithField("to", to). WithError(err).Error("Failed to list usage.") @@ -73,7 +82,7 @@ func (s *UsageService) ListBilledUsage(ctx context.Context, in *v1.ListBilledUsa } var billedSessions []*v1.BilledSession - for _, usageRecord := range usageRecords { + for _, usageRecord := range listUsageResult.UsageRecords { var endTime *timestamppb.Timestamp if usageRecord.StoppedAt.Valid { endTime = timestamppb.New(usageRecord.StoppedAt.Time) @@ -93,8 +102,19 @@ func (s *UsageService) ListBilledUsage(ctx context.Context, in *v1.ListBilledUsa billedSessions = append(billedSessions, billedSession) } + var totalPages = int64(math.Ceil(float64(listUsageResult.Count) / float64(limit))) + + var pagination = v1.PaginatedResponse{ + PerPage: limit, + Page: page, + TotalPages: totalPages, + Total: listUsageResult.Count, + } + return &v1.ListBilledUsageResponse{ - Sessions: billedSessions, + Sessions: billedSessions, + TotalCreditsUsed: listUsageResult.TotalCreditsUsed, + Pagination: &pagination, }, nil } diff --git a/components/usage/pkg/apiv1/usage_test.go b/components/usage/pkg/apiv1/usage_test.go index 31dc7e2cbdadd0..3445f029e738f6 100644 --- a/components/usage/pkg/apiv1/usage_test.go +++ b/components/usage/pkg/apiv1/usage_test.go @@ -170,6 +170,158 @@ func TestUsageService_ListBilledUsage(t *testing.T) { } } +func TestUsageService_ListBilledUsage_Pagination(t *testing.T) { + ctx := context.Background() + + type Expectation struct { + count int64 + total int64 + totalPages int64 + page int64 + perPage int64 + } + + type Scenario struct { + name string + Request *v1.ListBilledUsageRequest + Expect Expectation + } + + start := time.Date(2022, 07, 1, 13, 0, 0, 0, time.UTC) + attrID := db.NewTeamAttributionID(uuid.New().String()) + var instances []db.WorkspaceInstanceUsage + for i := 1; i <= 14; i++ { + instance := dbtest.NewWorkspaceInstanceUsage(t, db.WorkspaceInstanceUsage{ + AttributionID: attrID, + StartedAt: start.Add(time.Duration(i) * time.Minute), + StoppedAt: sql.NullTime{ + Time: start.Add(time.Duration(i)*time.Minute + time.Hour), + Valid: true, + }, + }) + instances = append(instances, instance) + } + + scenarios := []Scenario{ + (func() Scenario { + + return Scenario{ + name: "first page", + Request: &v1.ListBilledUsageRequest{ + AttributionId: string(attrID), + From: timestamppb.New(start), + To: timestamppb.New(start.Add(20*time.Minute + 2*time.Hour)), + Pagination: &v1.PaginatedRequest{ + PerPage: int64(5), + Page: int64(1), + }, + }, + Expect: Expectation{ + count: int64(5), + total: int64(14), + totalPages: int64(3), + page: int64(1), + perPage: int64(5), + }, + } + })(), + (func() Scenario { + + return Scenario{ + name: "second page", + Request: &v1.ListBilledUsageRequest{ + AttributionId: string(attrID), + From: timestamppb.New(start), + To: timestamppb.New(start.Add(20*time.Minute + 2*time.Hour)), + Pagination: &v1.PaginatedRequest{ + PerPage: int64(5), + Page: int64(2), + }, + }, + Expect: Expectation{ + count: int64(5), + total: int64(14), + totalPages: int64(3), + page: int64(2), + perPage: int64(5), + }, + } + })(), + (func() Scenario { + + return Scenario{ + name: "third page", + Request: &v1.ListBilledUsageRequest{ + AttributionId: string(attrID), + From: timestamppb.New(start), + To: timestamppb.New(start.Add(20*time.Minute + 2*time.Hour)), + Pagination: &v1.PaginatedRequest{ + PerPage: int64(5), + Page: int64(3), + }, + }, + Expect: Expectation{ + count: int64(4), + total: int64(14), + totalPages: int64(3), + page: int64(3), + perPage: int64(5), + }, + } + })(), + (func() Scenario { + + return Scenario{ + name: "fourth page", + Request: &v1.ListBilledUsageRequest{ + AttributionId: string(attrID), + From: timestamppb.New(start), + To: timestamppb.New(start.Add(20*time.Minute + 2*time.Hour)), + Pagination: &v1.PaginatedRequest{ + PerPage: int64(5), + Page: int64(4), + }, + }, + Expect: Expectation{ + count: int64(0), + total: int64(14), + totalPages: int64(3), + page: int64(4), + perPage: int64(5), + }, + } + })(), + } + + dbconn := dbtest.ConnectForTests(t) + dbtest.CreateWorkspaceInstanceUsageRecords(t, dbconn, instances...) + + srv := baseserver.NewForTests(t, + baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), + ) + + generator := NewReportGenerator(dbconn, DefaultWorkspacePricer) + v1.RegisterUsageServiceServer(srv.GRPC(), NewUsageService(dbconn, generator, nil)) + baseserver.StartServerForTests(t, srv) + + conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + + client := v1.NewUsageServiceClient(conn) + + resp, err := client.ListBilledUsage(ctx, scenario.Request) + require.NoError(t, err) + require.NotNil(t, resp.Pagination) + + require.Equal(t, scenario.Expect.total, resp.Pagination.Total) + require.Equal(t, scenario.Expect.totalPages, resp.Pagination.TotalPages) + }) + } +} + func TestInstanceToUsageRecords(t *testing.T) { maxStopTime := time.Date(2022, 05, 31, 23, 00, 00, 00, time.UTC) teamID, ownerID, projectID := uuid.New().String(), uuid.New(), uuid.New() diff --git a/components/usage/pkg/db/workspace_instance_usage.go b/components/usage/pkg/db/workspace_instance_usage.go index 1d7e81074ee138..d9975616e09df3 100644 --- a/components/usage/pkg/db/workspace_instance_usage.go +++ b/components/usage/pkg/db/workspace_instance_usage.go @@ -36,6 +36,12 @@ type WorkspaceInstanceUsage struct { Deleted bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"` } +type ListUsageResult struct { + UsageRecords []WorkspaceInstanceUsage + Count int64 + TotalCreditsUsed float64 +} + // TableName sets the insert table name for this struct type func (u *WorkspaceInstanceUsage) TableName() string { return "d_b_workspace_instance_usage" @@ -65,12 +71,47 @@ const ( AscendingOrder ) -func ListUsage(ctx context.Context, conn *gorm.DB, attributionId AttributionID, from, to time.Time, sort Order) ([]WorkspaceInstanceUsage, error) { +func ListUsage(ctx context.Context, conn *gorm.DB, attributionId AttributionID, from, to time.Time, sort Order, offset int64, limit int64) (*ListUsageResult, error) { + var listUsageResult = new(ListUsageResult) db := conn.WithContext(ctx) + var totalCreditsUsed sql.NullFloat64 + var count sql.NullInt64 + countResult, err := db. + WithContext(ctx). + Table((&WorkspaceInstanceUsage{}).TableName()). + Select("sum(creditsUsed) as totalCreditsUsed", "count(*) as count"). + Order(fmt.Sprintf("startedAt %s", sort.ToSQL())). + Where("attributionId = ?", attributionId). + Where( + // started before, finished inside query range + conn.Where("? <= stoppedAt AND stoppedAt < ?", from, to). + // started inside query range, finished inside + Or("startedAt >= ? AND stoppedAt < ?", from, to). + // started inside query range, finished outside + Or("? <= startedAt AND startedAt < ?", from, to). + // started before query range, still running + Or("startedAt <= ? AND (stoppedAt > ? OR stoppedAt IS NULL)", from, to), + ). + Rows() + if err != nil || !countResult.Next() { + return nil, fmt.Errorf("failed to get count of usage records: %s", err) + } + err = countResult.Scan(&totalCreditsUsed, &count) + if err != nil { + return nil, fmt.Errorf("failed to get count of usage records: %s", err) + } + if totalCreditsUsed.Valid { + listUsageResult.TotalCreditsUsed = totalCreditsUsed.Float64 + } + if count.Valid { + listUsageResult.Count = count.Int64 + } + var usageRecords []WorkspaceInstanceUsage result := db. WithContext(ctx). + Table((&WorkspaceInstanceUsage{}).TableName()). Order(fmt.Sprintf("startedAt %s", sort.ToSQL())). Where("attributionId = ?", attributionId). Where( @@ -83,9 +124,13 @@ func ListUsage(ctx context.Context, conn *gorm.DB, attributionId AttributionID, // started before query range, still running Or("startedAt <= ? AND (stoppedAt > ? OR stoppedAt IS NULL)", from, to), ). + Offset(int(offset)). + Limit(int(limit)). Find(&usageRecords) if result.Error != nil { return nil, fmt.Errorf("failed to get usage records: %s", result.Error) } - return usageRecords, nil + listUsageResult.UsageRecords = usageRecords + + return listUsageResult, nil } diff --git a/components/usage/pkg/db/workspace_instance_usage_test.go b/components/usage/pkg/db/workspace_instance_usage_test.go index 8b07ec2e078c10..264bc02755ecd0 100644 --- a/components/usage/pkg/db/workspace_instance_usage_test.go +++ b/components/usage/pkg/db/workspace_instance_usage_test.go @@ -54,16 +54,19 @@ func TestCreateUsageRecords_Updates(t *testing.T) { Valid: true, }, }) - require.NoError(t, db.CreateUsageRecords(context.Background(), conn, []db.WorkspaceInstanceUsage{update})) t.Cleanup(func() { conn.Model(&db.WorkspaceInstanceUsage{}).Delete(update) }) - list, err := db.ListUsage(context.Background(), conn, teamAttributionID, time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC), time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC), db.DescendingOrder) + listResult, err := db.ListUsage(context.Background(), conn, teamAttributionID, time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC), time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC), db.DescendingOrder, int64(0), int64(100)) require.NoError(t, err) - require.Len(t, list, 1) - require.Equal(t, update, list[0]) + if err == nil { + + } + require.Len(t, listResult.UsageRecords, 1) + require.Equal(t, int64(1), listResult.Count) + require.Equal(t, update, listResult.UsageRecords[0]) } func TestListUsage_Ordering(t *testing.T) { @@ -91,10 +94,10 @@ func TestListUsage_Ordering(t *testing.T) { conn.Model(&db.WorkspaceInstanceUsage{}).Delete(instances) }) - listed, err := db.ListUsage(context.Background(), conn, teamAttributionID, time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC), time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC), db.AscendingOrder) + listResult, err := db.ListUsage(context.Background(), conn, teamAttributionID, time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC), time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC), db.AscendingOrder, int64(0), int64(100)) require.NoError(t, err) - require.Equal(t, []db.WorkspaceInstanceUsage{oldest, newest}, listed) + require.Equal(t, []db.WorkspaceInstanceUsage{oldest, newest}, listResult.UsageRecords) } func TestListUsageInRange(t *testing.T) { @@ -178,11 +181,45 @@ func TestListUsageInRange(t *testing.T) { instances := []db.WorkspaceInstanceUsage{startBeforeFinishBefore, startBeforeFinishInside, startInsideFinishInside, startInsideFinishInsideButDifferentAttributionID, startedInsideFinishedOutside, startedOutsideFinishedOutside, startedBeforeAndStillRunning, startedInsideAndStillRunning} dbtest.CreateWorkspaceInstanceUsageRecords(t, conn, instances...) - results, err := db.ListUsage(context.Background(), conn, attributionID, start, end, db.DescendingOrder) + listResult, err := db.ListUsage(context.Background(), conn, attributionID, start, end, db.DescendingOrder, int64(0), int64(100)) require.NoError(t, err) - require.Len(t, results, 5) + require.Len(t, listResult.UsageRecords, 5) require.Equal(t, []db.WorkspaceInstanceUsage{ startedInsideFinishedOutside, startedInsideAndStillRunning, startInsideFinishInside, startBeforeFinishInside, startedBeforeAndStillRunning, - }, results) + }, listResult.UsageRecords) +} + +func TestListUsagePagination(t *testing.T) { + conn := dbtest.ConnectForTests(t) + + start := time.Date(2022, 7, 1, 0, 0, 0, 0, time.UTC) + end := time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC) + + attributionID := db.NewTeamAttributionID(uuid.New().String()) + + // started inside query range, and also finished inside query range + startInsideFinishInside := dbtest.NewWorkspaceInstanceUsage(t, db.WorkspaceInstanceUsage{ + AttributionID: attributionID, + StartedAt: start.Add(3 * time.Hour), + StoppedAt: sql.NullTime{ + Time: start.Add(5 * time.Hour), + Valid: true, + }, + CreditsUsed: float64(2), + }) + + var instances []db.WorkspaceInstanceUsage + for i := 0; i < 122; i++ { + startInsideFinishInside.InstanceID = uuid.New() + instances = append(instances, startInsideFinishInside) + } + dbtest.CreateWorkspaceInstanceUsageRecords(t, conn, instances...) + + listResult, err := db.ListUsage(context.Background(), conn, attributionID, start, end, db.DescendingOrder, int64(100), int64(50)) + require.NoError(t, err) + + require.Len(t, listResult.UsageRecords, 22) + require.Equal(t, float64(244), listResult.TotalCreditsUsed) + require.Equal(t, int64(122), listResult.Count) }