Skip to content

Commit a8d0889

Browse files
AlexTugarevroboquat
authored andcommitted
Add pagination to list usage
1 parent a2fa9dc commit a8d0889

File tree

15 files changed

+1388
-278
lines changed

15 files changed

+1388
-278
lines changed

components/dashboard/src/Pagination/Pagination.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,19 @@ import { getPaginationNumbers } from "./getPagination";
88
import Arrow from "../components/Arrow";
99

1010
interface PaginationProps {
11-
totalResults: number;
1211
totalNumberOfPages: number;
1312
currentPage: number;
14-
setCurrentPage: any;
13+
setPage: (page: number) => void;
1514
}
1615

17-
function Pagination({ totalNumberOfPages, currentPage, setCurrentPage }: PaginationProps) {
16+
function Pagination({ totalNumberOfPages, currentPage, setPage }: PaginationProps) {
1817
const calculatedPagination = getPaginationNumbers(totalNumberOfPages, currentPage);
1918

2019
const nextPage = () => {
21-
if (currentPage !== totalNumberOfPages) setCurrentPage(currentPage + 1);
20+
if (currentPage !== totalNumberOfPages) setPage(currentPage + 1);
2221
};
2322
const prevPage = () => {
24-
if (currentPage !== 1) setCurrentPage(currentPage - 1);
23+
if (currentPage !== 1) setPage(currentPage - 1);
2524
};
2625
const getClassnames = (pageNumber: string | number) => {
2726
if (pageNumber === currentPage) {
@@ -47,8 +46,8 @@ function Pagination({ totalNumberOfPages, currentPage, setCurrentPage }: Paginat
4746
return <li className={getClassnames(pn)}>&#8230;</li>;
4847
}
4948
return (
50-
<li key={i} className={getClassnames(pn)}>
51-
<span onClick={() => setCurrentPage(pn)}>{pn}</span>
49+
<li key={i} className={getClassnames(pn)} onClick={() => typeof pn === "number" && setPage(pn)}>
50+
<span>{pn}</span>
5251
</li>
5352
);
5453
})}

components/dashboard/src/teams/TeamUsage.tsx

+53-65
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { useLocation } from "react-router";
99
import { getCurrentTeam, TeamsContext } from "./teams-context";
1010
import { getGitpodService, gitpodHostUrl } from "../service/service";
1111
import {
12-
BillableSessionRequest,
12+
ListBilledUsageRequest,
1313
BillableWorkspaceType,
1414
ExtendedBillableSession,
15-
SortOrder,
15+
ListBilledUsageResponse,
1616
} from "@gitpod/gitpod-protocol/lib/usage";
1717
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
1818
import { Item, ItemField, ItemsList } from "../components/ItemsList";
@@ -21,7 +21,6 @@ import Header from "../components/Header";
2121
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
2222
import { ReactComponent as CreditsSvg } from "../images/credits.svg";
2323
import { ReactComponent as Spinner } from "../icons/Spinner.svg";
24-
import { ReactComponent as SortArrow } from "../images/sort-arrow.svg";
2524
import { ReactComponent as UsageIcon } from "../images/usage-default.svg";
2625
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
2726
import { toRemoteURL } from "../projects/render-utils";
@@ -31,17 +30,15 @@ function TeamUsage() {
3130
const location = useLocation();
3231
const team = getCurrentTeam(location, teams);
3332
const [teamBillingMode, setTeamBillingMode] = useState<BillingMode | undefined>(undefined);
34-
const [billedUsage, setBilledUsage] = useState<ExtendedBillableSession[]>([]);
35-
const [currentPage, setCurrentPage] = useState(1);
36-
const [resultsPerPage] = useState(50);
33+
const [usagePage, setUsagePage] = useState<ListBilledUsageResponse | undefined>(undefined);
3734
const [errorMessage, setErrorMessage] = useState("");
3835
const today = new Date();
3936
const startOfCurrentMonth = new Date(today.getFullYear(), today.getMonth(), 1);
4037
const timestampStartOfCurrentMonth = startOfCurrentMonth.getTime();
4138
const [startDateOfBillMonth, setStartDateOfBillMonth] = useState(timestampStartOfCurrentMonth);
4239
const [endDateOfBillMonth, setEndDateOfBillMonth] = useState(Date.now());
40+
const [totalCreditsUsed, setTotalCreditsUsed] = useState<number>(0);
4341
const [isLoading, setIsLoading] = useState<boolean>(true);
44-
const [isStartedTimeDescending, setIsStartedTimeDescending] = useState<boolean>(true);
4542

4643
useEffect(() => {
4744
if (!team) {
@@ -57,30 +54,8 @@ function TeamUsage() {
5754
if (!team) {
5855
return;
5956
}
60-
if (billedUsage.length === 0) {
61-
setIsLoading(true);
62-
}
63-
(async () => {
64-
const attributionId = AttributionId.render({ kind: "team", teamId: team.id });
65-
const request: BillableSessionRequest = {
66-
attributionId,
67-
startedTimeOrder: isStartedTimeDescending ? SortOrder.Descending : SortOrder.Ascending,
68-
from: startDateOfBillMonth,
69-
to: endDateOfBillMonth,
70-
};
71-
try {
72-
const { server } = getGitpodService();
73-
const billedUsageResult = await server.listBilledUsage(request);
74-
setBilledUsage(billedUsageResult);
75-
} catch (error) {
76-
if (error.code === ErrorCodes.PERMISSION_DENIED) {
77-
setErrorMessage("Access to usage details is restricted to team owners.");
78-
}
79-
} finally {
80-
setIsLoading(false);
81-
}
82-
})();
83-
}, [team, startDateOfBillMonth, endDateOfBillMonth, isStartedTimeDescending]);
57+
loadPage(1);
58+
}, [team, startDateOfBillMonth, endDateOfBillMonth]);
8459

8560
useEffect(() => {
8661
if (!teamBillingMode) {
@@ -91,6 +66,37 @@ function TeamUsage() {
9166
}
9267
}, [teamBillingMode]);
9368

69+
const loadPage = async (page: number = 1) => {
70+
if (!team) {
71+
return;
72+
}
73+
if (usagePage === undefined) {
74+
setIsLoading(true);
75+
setTotalCreditsUsed(0);
76+
}
77+
const attributionId = AttributionId.render({ kind: "team", teamId: team.id });
78+
const request: ListBilledUsageRequest = {
79+
attributionId,
80+
fromDate: startDateOfBillMonth,
81+
toDate: endDateOfBillMonth,
82+
perPage: 50,
83+
page,
84+
};
85+
try {
86+
const page = await getGitpodService().server.listBilledUsage(request);
87+
setUsagePage(page);
88+
setTotalCreditsUsed(Math.ceil(page.totalCreditsUsed));
89+
} catch (error) {
90+
if (error.code === ErrorCodes.PERMISSION_DENIED) {
91+
setErrorMessage("Access to usage details is restricted to team owners.");
92+
} else {
93+
setErrorMessage(`Error: ${error?.message}`);
94+
}
95+
} finally {
96+
setIsLoading(false);
97+
}
98+
};
99+
94100
const getType = (type: BillableWorkspaceType) => {
95101
if (type === "regular") {
96102
return "Workspace";
@@ -111,12 +117,6 @@ function TeamUsage() {
111117
return inMinutes + " min";
112118
};
113119

114-
const calculateTotalUsage = () => {
115-
let totalCredits = 0;
116-
billedUsage.forEach((session) => (totalCredits += session.credits));
117-
return totalCredits.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
118-
};
119-
120120
const handleMonthClick = (start: any, end: any) => {
121121
setStartDateOfBillMonth(start);
122122
setEndDateOfBillMonth(end);
@@ -155,10 +155,7 @@ function TeamUsage() {
155155
return new Date(time).toLocaleDateString(undefined, options).replace("at ", "");
156156
};
157157

158-
const lastResultOnCurrentPage = currentPage * resultsPerPage;
159-
const firstResultOnCurrentPage = lastResultOnCurrentPage - resultsPerPage;
160-
const totalNumberOfPages = Math.ceil(billedUsage.length / resultsPerPage);
161-
const currentPaginatedResults = billedUsage.slice(firstResultOnCurrentPage, lastResultOnCurrentPage);
158+
const currentPaginatedResults = usagePage?.sessions ?? [];
162159

163160
return (
164161
<>
@@ -181,16 +178,18 @@ function TeamUsage() {
181178
<div className="text-base text-gray-500 truncate">Previous Months</div>
182179
{getBillingHistory()}
183180
</div>
184-
<div className="flex flex-col truncate">
185-
<div className="text-base text-gray-500">Total usage</div>
186-
<div className="flex text-lg text-gray-600 font-semibold">
187-
<CreditsSvg className="my-auto mr-1" />
188-
<span>{calculateTotalUsage()} Credits</span>
181+
{!isLoading && (
182+
<div className="flex flex-col truncate">
183+
<div className="text-base text-gray-500">Total usage</div>
184+
<div className="flex text-lg text-gray-600 font-semibold">
185+
<CreditsSvg className="my-auto mr-1" />
186+
<span>{totalCreditsUsed} Credits</span>
187+
</div>
189188
</div>
190-
</div>
189+
)}
191190
</div>
192191
</div>
193-
{!isLoading && billedUsage.length === 0 && !errorMessage && (
192+
{!isLoading && usagePage === undefined && !errorMessage && (
194193
<div className="flex flex-col w-full mb-8">
195194
<h3 className="text-center text-gray-500 mt-8">No sessions found.</h3>
196195
<p className="text-center text-gray-500 mt-1">
@@ -215,7 +214,7 @@ function TeamUsage() {
215214
<Spinner className="m-2 h-5 w-5 animate-spin" />
216215
</div>
217216
)}
218-
{billedUsage.length > 0 && !isLoading && (
217+
{!isLoading && currentPaginatedResults.length > 0 && (
219218
<div className="flex flex-col w-full mb-8">
220219
<ItemsList className="mt-2 text-gray-400 dark:text-gray-500">
221220
<Item
@@ -233,17 +232,7 @@ function TeamUsage() {
233232
</ItemField>
234233
<ItemField className="my-auto" />
235234
<ItemField className="col-span-3 my-auto cursor-pointer">
236-
<span
237-
className="flex my-auto"
238-
onClick={() => setIsStartedTimeDescending(!isStartedTimeDescending)}
239-
>
240-
Timestamp
241-
<SortArrow
242-
className={`ml-2 h-4 w-4 my-auto ${
243-
isStartedTimeDescending ? "" : " transform rotate-180"
244-
}`}
245-
/>
246-
</span>
235+
<span>Timestamp</span>
247236
</ItemField>
248237
</Item>
249238
{currentPaginatedResults &&
@@ -310,12 +299,11 @@ function TeamUsage() {
310299
);
311300
})}
312301
</ItemsList>
313-
{billedUsage.length > resultsPerPage && (
302+
{usagePage && usagePage.totalPages > 1 && (
314303
<Pagination
315-
totalResults={billedUsage.length}
316-
currentPage={currentPage}
317-
setCurrentPage={setCurrentPage}
318-
totalNumberOfPages={totalNumberOfPages}
304+
currentPage={usagePage.page}
305+
setPage={(page) => loadPage(page)}
306+
totalNumberOfPages={usagePage.totalPages}
319307
/>
320308
)}
321309
</div>

components/gitpod-protocol/src/gitpod-service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./
6161
import { IDEServer } from "./ide-protocol";
6262
import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol";
6363
import { Currency } from "./plans";
64-
import { BillableSession, BillableSessionRequest } from "./usage";
64+
import { ListBilledUsageResponse, ListBilledUsageRequest } from "./usage";
6565
import { SupportedWorkspaceClass } from "./workspace-class";
6666
import { BillingMode } from "./billing-mode";
6767

@@ -297,7 +297,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
297297
getSpendingLimitForTeam(teamId: string): Promise<number | undefined>;
298298
setSpendingLimitForTeam(teamId: string, spendingLimit: number): Promise<void>;
299299

300-
listBilledUsage(req: BillableSessionRequest): Promise<BillableSession[]>;
300+
listBilledUsage(req: ListBilledUsageRequest): Promise<ListBilledUsageResponse>;
301301

302302
setUsageAttribution(usageAttribution: string): Promise<void>;
303303

components/gitpod-protocol/src/usage.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,24 @@ export interface ExtendedBillableSession extends BillableSession {
4040
user?: Pick<User.Profile, "name" | "avatarURL">;
4141
}
4242

43-
export interface BillableSessionRequest {
43+
/**
44+
* This is a paginated request
45+
*/
46+
export interface ListBilledUsageRequest {
4447
attributionId: string;
45-
startedTimeOrder: SortOrder;
46-
from?: number;
47-
to?: number;
48+
fromDate?: number;
49+
toDate?: number;
50+
perPage: number;
51+
page: number;
4852
}
4953

50-
export type BillableWorkspaceType = WorkspaceType;
51-
52-
export enum SortOrder {
53-
Descending = 0,
54-
Ascending = 1,
54+
export interface ListBilledUsageResponse {
55+
sessions: ExtendedBillableSession[];
56+
totalCreditsUsed: number;
57+
totalPages: number;
58+
totalSessions: number;
59+
perPage: number;
60+
page: number;
5561
}
62+
63+
export type BillableWorkspaceType = WorkspaceType;

components/server/ee/src/workspace/gitpod-server-impl.ts

+39-25
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor
7171
import { EligibilityService } from "../user/eligibility-service";
7272
import { AccountStatementProvider } from "../user/account-statement-provider";
7373
import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol";
74-
import { ExtendedBillableSession, BillableSessionRequest } from "@gitpod/gitpod-protocol/lib/usage";
74+
import { ListBilledUsageRequest, ListBilledUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
75+
import { ListBilledUsageRequest as ListBilledUsage } from "@gitpod/usage-api/lib/usage/v1/usage_pb";
7576
import {
7677
AssigneeIdentityIdentifier,
7778
TeamSubscription,
@@ -2149,48 +2150,61 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21492150
return result;
21502151
}
21512152

2152-
async listBilledUsage(ctx: TraceContext, req: BillableSessionRequest): Promise<ExtendedBillableSession[]> {
2153-
const { attributionId, startedTimeOrder, from, to } = req;
2153+
async listBilledUsage(ctx: TraceContext, req: ListBilledUsageRequest): Promise<ListBilledUsageResponse> {
2154+
const { attributionId, fromDate, toDate, perPage, page } = req;
21542155
traceAPIParams(ctx, { attributionId });
21552156
let timestampFrom;
21562157
let timestampTo;
21572158
const user = this.checkAndBlockUser("listBilledUsage");
21582159

21592160
await this.guardCostCenterAccess(ctx, user.id, attributionId, "get");
21602161

2161-
if (from) {
2162-
timestampFrom = Timestamp.fromDate(new Date(from));
2162+
if (fromDate) {
2163+
timestampFrom = Timestamp.fromDate(new Date(fromDate));
21632164
}
2164-
if (to) {
2165-
timestampTo = Timestamp.fromDate(new Date(to));
2165+
if (toDate) {
2166+
timestampTo = Timestamp.fromDate(new Date(toDate));
21662167
}
21672168
const usageClient = this.usageServiceClientProvider.getDefault();
21682169
const response = await usageClient.listBilledUsage(
21692170
ctx,
21702171
attributionId,
2171-
startedTimeOrder as number,
2172+
ListBilledUsage.Ordering.ORDERING_DESCENDING,
2173+
perPage,
2174+
page,
21722175
timestampFrom,
21732176
timestampTo,
21742177
);
2175-
const sessions = response.getSessionsList().map((s) => UsageService.mapBilledSession(s));
2176-
const extendedSessions = await Promise.all(
2177-
sessions.map(async (session) => {
2178-
const ws = await this.workspaceDb.trace(ctx).findWorkspaceAndInstance(session.workspaceId);
2179-
let profile: User.Profile | undefined = undefined;
2180-
if (session.workspaceType === "regular" && session.userId) {
2181-
const user = await this.userDB.findUserById(session.userId);
2182-
if (user) {
2183-
profile = User.getProfile(user);
2178+
const sessions = await Promise.all(
2179+
response
2180+
.getSessionsList()
2181+
.map((s) => UsageService.mapBilledSession(s))
2182+
.map(async (session) => {
2183+
const ws = await this.workspaceDb.trace(ctx).findWorkspaceAndInstance(session.workspaceId);
2184+
let profile: User.Profile | undefined = undefined;
2185+
if (session.workspaceType === "regular" && session.userId) {
2186+
// TODO add caching to void repeated loading of same profile details here
2187+
const user = await this.userDB.findUserById(session.userId);
2188+
if (user) {
2189+
profile = User.getProfile(user);
2190+
}
21842191
}
2185-
}
2186-
return {
2187-
...session,
2188-
contextURL: ws?.contextURL,
2189-
user: profile ? <User.Profile>{ name: profile.name, avatarURL: profile.avatarURL } : undefined,
2190-
};
2191-
}),
2192+
return {
2193+
...session,
2194+
contextURL: ws?.contextURL,
2195+
user: profile,
2196+
};
2197+
}),
21922198
);
2193-
return extendedSessions;
2199+
const pagination = response.getPagination();
2200+
return {
2201+
sessions,
2202+
totalSessions: pagination?.getTotal() || 0,
2203+
totalPages: pagination?.getTotalPages() || 0,
2204+
page: pagination?.getPage() || 0,
2205+
perPage: pagination?.getPerPage() || 0,
2206+
totalCreditsUsed: response.getTotalCreditsUsed(),
2207+
};
21942208
}
21952209

21962210
protected async guardCostCenterAccess(

0 commit comments

Comments
 (0)