Skip to content

Commit 24fe829

Browse files
authored
[dashboard] cache usage queries (#16746)
1 parent 4fafa16 commit 24fe829

File tree

3 files changed

+87
-70
lines changed

3 files changed

+87
-70
lines changed

components/dashboard/src/Pagination/Pagination.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ function Pagination(props: PaginationProps) {
4848
/>
4949
{calculatedPagination.map((pn, i) => {
5050
if (pn === "...") {
51-
return <li className={getClassnames(pn)}>&#8230;</li>;
51+
return (
52+
<li key={i} className={getClassnames(pn)}>
53+
&#8230;
54+
</li>
55+
);
5256
}
5357
return (
5458
<li key={i} className={getClassnames(pn)} onClick={() => typeof pn === "number" && setPage(pn)}>

components/dashboard/src/components/UsageView.tsx

Lines changed: 54 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,38 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { forwardRef, useCallback, useEffect, useState } from "react";
8-
import { getGitpodService, gitpodHostUrl } from "../service/service";
9-
import {
10-
ListUsageRequest,
11-
Ordering,
12-
ListUsageResponse,
13-
WorkspaceInstanceUsageData,
14-
Usage,
15-
} from "@gitpod/gitpod-protocol/lib/usage";
7+
import { WorkspaceType } from "@gitpod/gitpod-protocol";
168
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
17-
import { Item, ItemField, ItemsList } from "../components/ItemsList";
18-
import Pagination from "../Pagination/Pagination";
19-
import Header from "../components/Header";
209
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
10+
import { ListUsageRequest, Ordering, Usage, WorkspaceInstanceUsageData } from "@gitpod/gitpod-protocol/lib/usage";
11+
import dayjs from "dayjs";
12+
import { forwardRef, useEffect, useMemo, useState } from "react";
13+
import DatePicker from "react-datepicker";
14+
import "react-datepicker/dist/react-datepicker.css";
15+
import { useLocation } from "react-router";
16+
import Header from "../components/Header";
17+
import { Item, ItemField, ItemsList } from "../components/ItemsList";
18+
import { useListUsage } from "../data/usage/usage-query";
19+
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";
2120
import Spinner from "../icons/Spinner.svg";
2221
import { ReactComponent as UsageIcon } from "../images/usage-default.svg";
22+
import Pagination from "../Pagination/Pagination";
2323
import { toRemoteURL } from "../projects/render-utils";
24-
import { WorkspaceType } from "@gitpod/gitpod-protocol";
25-
import DatePicker from "react-datepicker";
26-
import "react-datepicker/dist/react-datepicker.css";
24+
import { gitpodHostUrl } from "../service/service";
2725
import "./react-datepicker.css";
28-
import { useLocation } from "react-router";
29-
import dayjs from "dayjs";
3026
import { Heading1, Heading2, Subheading } from "./typography/headings";
31-
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";
3227

3328
interface UsageViewProps {
3429
attributionId: AttributionId;
3530
}
3631

3732
function UsageView({ attributionId }: UsageViewProps) {
38-
const [usagePage, setUsagePage] = useState<ListUsageResponse | undefined>(undefined);
33+
const [page, setPage] = useState(1);
3934
const [errorMessage, setErrorMessage] = useState("");
4035
const startOfCurrentMonth = dayjs().startOf("month");
4136
const [startDate, setStartDate] = useState(startOfCurrentMonth);
4237
const [endDate, setEndDate] = useState(dayjs());
43-
const [totalCreditsUsed, setTotalCreditsUsed] = useState<number>(0);
44-
const [isLoading, setIsLoading] = useState<boolean>(true);
4538
const supportedClasses = useWorkspaceClasses();
46-
4739
const location = useLocation();
4840
useEffect(() => {
4941
const match = /#(\d{4}-\d{2}-\d{2}):(\d{4}-\d{2}-\d{2})/.exec(location.hash);
@@ -56,39 +48,29 @@ function UsageView({ attributionId }: UsageViewProps) {
5648
}
5749
}
5850
}, [location]);
51+
const request = useMemo(() => {
52+
const request: ListUsageRequest = {
53+
attributionId: AttributionId.render(attributionId),
54+
from: startDate.startOf("day").valueOf(),
55+
to: endDate.endOf("day").valueOf(),
56+
order: Ordering.ORDERING_DESCENDING,
57+
pagination: {
58+
perPage: 50,
59+
page,
60+
},
61+
};
62+
return request;
63+
}, [attributionId, endDate, page, startDate]);
64+
const usagePage = useListUsage(request);
65+
66+
if (usagePage.error) {
67+
if ((usagePage.error as any).code === ErrorCodes.PERMISSION_DENIED) {
68+
setErrorMessage("Access to usage details is restricted to team owners.");
69+
} else {
70+
setErrorMessage(`Error: ${usagePage.error?.message}`);
71+
}
72+
}
5973

60-
const loadPage = useCallback(
61-
async (page: number = 1) => {
62-
if (usagePage === undefined) {
63-
setIsLoading(true);
64-
setTotalCreditsUsed(0);
65-
}
66-
const request: ListUsageRequest = {
67-
attributionId: AttributionId.render(attributionId),
68-
from: startDate.startOf("day").valueOf(),
69-
to: endDate.endOf("day").valueOf(),
70-
order: Ordering.ORDERING_DESCENDING,
71-
pagination: {
72-
perPage: 50,
73-
page,
74-
},
75-
};
76-
try {
77-
const page = await getGitpodService().server.listUsage(request);
78-
setUsagePage(page);
79-
setTotalCreditsUsed(page.creditsUsed);
80-
} catch (error) {
81-
if (error.code === ErrorCodes.PERMISSION_DENIED) {
82-
setErrorMessage("Access to usage details is restricted to team owners.");
83-
} else {
84-
setErrorMessage(`Error: ${error?.message}`);
85-
}
86-
} finally {
87-
setIsLoading(false);
88-
}
89-
},
90-
[attributionId, endDate, startDate, usagePage],
91-
);
9274
useEffect(() => {
9375
if (startDate.isAfter(endDate)) {
9476
setErrorMessage("The start date needs to be before the end date.");
@@ -99,8 +81,8 @@ function UsageView({ attributionId }: UsageViewProps) {
9981
return;
10082
}
10183
setErrorMessage("");
102-
loadPage(1);
103-
}, [startDate, endDate, loadPage]);
84+
setPage(1);
85+
}, [startDate, endDate, setPage]);
10486

10587
const getType = (type: WorkspaceType) => {
10688
if (type === "regular") {
@@ -172,7 +154,8 @@ function UsageView({ attributionId }: UsageViewProps) {
172154
return new Date(time).toLocaleDateString(undefined, options).replace("at ", "");
173155
};
174156

175-
const currentPaginatedResults = usagePage?.usageEntriesList.filter((u) => u.kind === "workspaceinstance") ?? [];
157+
const currentPaginatedResults =
158+
usagePage.data?.usageEntriesList.filter((u) => u.kind === "workspaceinstance") ?? [];
176159
const DateDisplay = forwardRef((arg: any, ref: any) => (
177160
<div
178161
className="px-2 py-0.5 text-gray-500 bg-gray-50 dark:text-gray-400 dark:bg-gray-800 rounded-md cursor-pointer flex items-center hover:bg-gray-100 dark:hover:bg-gray-700"
@@ -253,21 +236,21 @@ function UsageView({ attributionId }: UsageViewProps) {
253236
<div className="text-base text-gray-500 truncate">Previous Months</div>
254237
{getBillingHistory()}
255238
</div>
256-
{!isLoading && (
239+
{!usagePage.isLoading && (
257240
<div>
258241
<div className="flex flex-col truncate">
259242
<div className="text-base text-gray-500">Credits</div>
260243
<div className="flex text-lg text-gray-600 font-semibold">
261244
<span className="dark:text-gray-400">
262-
{totalCreditsUsed.toLocaleString()}
245+
{usagePage.data?.creditsUsed.toLocaleString()}
263246
</span>
264247
</div>
265248
</div>
266249
</div>
267250
)}
268251
</div>
269252
</div>
270-
{!isLoading &&
253+
{!usagePage.isLoading &&
271254
(usagePage === undefined || currentPaginatedResults.length === 0) &&
272255
!errorMessage && (
273256
<div className="flex flex-col w-full mb-8">
@@ -282,13 +265,13 @@ function UsageView({ attributionId }: UsageViewProps) {
282265
</Subheading>
283266
</div>
284267
)}
285-
{isLoading && (
268+
{usagePage.isLoading && (
286269
<div className="flex items-center justify-center w-full space-x-2 text-gray-400 text-sm pt-16 pb-40">
287270
<img alt="Loading Spinner" className="h-4 w-4 animate-spin" src={Spinner} />
288271
<span>Fetching usage...</span>
289272
</div>
290273
)}
291-
{!isLoading && currentPaginatedResults.length > 0 && (
274+
{!usagePage.isLoading && currentPaginatedResults.length > 0 && (
292275
<div className="flex flex-col w-full mb-8">
293276
<ItemsList className="mt-2 text-gray-400 dark:text-gray-500">
294277
<Item
@@ -402,13 +385,15 @@ function UsageView({ attributionId }: UsageViewProps) {
402385
);
403386
})}
404387
</ItemsList>
405-
{usagePage && usagePage.pagination && usagePage.pagination.totalPages > 1 && (
406-
<Pagination
407-
currentPage={usagePage.pagination.page}
408-
setPage={(page) => loadPage(page)}
409-
totalNumberOfPages={usagePage.pagination.totalPages}
410-
/>
411-
)}
388+
{usagePage.data &&
389+
usagePage.data.pagination &&
390+
usagePage.data.pagination.totalPages > 1 && (
391+
<Pagination
392+
currentPage={usagePage.data.pagination.page}
393+
setPage={setPage}
394+
totalNumberOfPages={usagePage.data.pagination.totalPages}
395+
/>
396+
)}
412397
</div>
413398
)}
414399
</div>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
8+
import { useQuery } from "@tanstack/react-query";
9+
import { getGitpodService } from "../../service/service";
10+
11+
export function useListUsage(request?: ListUsageRequest) {
12+
const query = useQuery<ListUsageResponse, Error>(
13+
["usage", request],
14+
() => {
15+
console.log("Fetching usage... ", request);
16+
if (!request) {
17+
throw new Error("request is required");
18+
}
19+
return getGitpodService().server.listUsage(request);
20+
},
21+
{
22+
enabled: !!request,
23+
cacheTime: 1000 * 60 * 10, // 10 minutes
24+
staleTime: 1000 * 60 * 10, // 10 minutes
25+
},
26+
);
27+
return query;
28+
}

0 commit comments

Comments
 (0)