Skip to content

Commit f87e6b6

Browse files
committed
Add pagination to list usage
1 parent 1a65e69 commit f87e6b6

File tree

15 files changed

+755
-214
lines changed

15 files changed

+755
-214
lines changed

components/dashboard/src/Pagination/Pagination.tsx

Lines changed: 6 additions & 7 deletions
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

Lines changed: 55 additions & 65 deletions
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,39 @@ 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+
}
93+
} finally {
94+
setIsLoading(false);
95+
}
96+
};
97+
98+
const handleSetPage = async (page: number) => {
99+
await loadPage(page);
100+
};
101+
94102
const getType = (type: BillableWorkspaceType) => {
95103
if (type === "regular") {
96104
return "Workspace";
@@ -111,12 +119,6 @@ function TeamUsage() {
111119
return inMinutes + " min";
112120
};
113121

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-
120122
const handleMonthClick = (start: any, end: any) => {
121123
setStartDateOfBillMonth(start);
122124
setEndDateOfBillMonth(end);
@@ -155,10 +157,7 @@ function TeamUsage() {
155157
return new Date(time).toLocaleDateString(undefined, options).replace("at ", "");
156158
};
157159

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);
160+
const currentPaginatedResults = usagePage?.sessions ?? [];
162161

163162
return (
164163
<>
@@ -181,16 +180,18 @@ function TeamUsage() {
181180
<div className="text-base text-gray-500 truncate">Previous Months</div>
182181
{getBillingHistory()}
183182
</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>
183+
{!isLoading && (
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>{totalCreditsUsed} Credits</span>
189+
</div>
189190
</div>
190-
</div>
191+
)}
191192
</div>
192193
</div>
193-
{!isLoading && billedUsage.length === 0 && !errorMessage && (
194+
{!isLoading && usagePage === undefined && !errorMessage && (
194195
<div className="flex flex-col w-full mb-8">
195196
<h3 className="text-center text-gray-500 mt-8">No sessions found.</h3>
196197
<p className="text-center text-gray-500 mt-1">
@@ -215,7 +216,7 @@ function TeamUsage() {
215216
<Spinner className="m-2 h-5 w-5 animate-spin" />
216217
</div>
217218
)}
218-
{billedUsage.length > 0 && !isLoading && (
219+
{!isLoading && currentPaginatedResults.length > 0 && (
219220
<div className="flex flex-col w-full mb-8">
220221
<ItemsList className="mt-2 text-gray-400 dark:text-gray-500">
221222
<Item
@@ -233,17 +234,7 @@ function TeamUsage() {
233234
</ItemField>
234235
<ItemField className="my-auto" />
235236
<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>
237+
<span>Timestamp</span>
247238
</ItemField>
248239
</Item>
249240
{currentPaginatedResults &&
@@ -310,12 +301,11 @@ function TeamUsage() {
310301
);
311302
})}
312303
</ItemsList>
313-
{billedUsage.length > resultsPerPage && (
304+
{usagePage && usagePage.totalPages > 1 && (
314305
<Pagination
315-
totalResults={billedUsage.length}
316-
currentPage={currentPage}
317-
setCurrentPage={setCurrentPage}
318-
totalNumberOfPages={totalNumberOfPages}
306+
currentPage={usagePage.page}
307+
setPage={(page) => handleSetPage(page)}
308+
totalNumberOfPages={usagePage.totalPages}
319309
/>
320310
)}
321311
</div>

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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 17 additions & 9 deletions
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+
total: number;
59+
perPage: number;
60+
page: number;
5561
}
62+
63+
export type BillableWorkspaceType = WorkspaceType;

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

Lines changed: 38 additions & 25 deletions
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,60 @@ 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+
return {
2200+
sessions,
2201+
total: response.getTotal(),
2202+
totalPages: response.getTotalPages(),
2203+
page: response.getPage(),
2204+
perPage: response.getPerPage(),
2205+
totalCreditsUsed: response.getTotalCreditsUsed(),
2206+
};
21942207
}
21952208

21962209
protected async guardCostCenterAccess(

0 commit comments

Comments
 (0)