Skip to content

[dashboard] cache usage queries #16746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion components/dashboard/src/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ function Pagination(props: PaginationProps) {
/>
{calculatedPagination.map((pn, i) => {
if (pn === "...") {
return <li className={getClassnames(pn)}>&#8230;</li>;
return (
<li key={i} className={getClassnames(pn)}>
&#8230;
</li>
);
}
return (
<li key={i} className={getClassnames(pn)} onClick={() => typeof pn === "number" && setPage(pn)}>
Expand Down
123 changes: 54 additions & 69 deletions components/dashboard/src/components/UsageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,38 @@
* See License.AGPL.txt in the project root for license information.
*/

import { forwardRef, useCallback, useEffect, useState } from "react";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import {
ListUsageRequest,
Ordering,
ListUsageResponse,
WorkspaceInstanceUsageData,
Usage,
} from "@gitpod/gitpod-protocol/lib/usage";
import { WorkspaceType } from "@gitpod/gitpod-protocol";
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
import { Item, ItemField, ItemsList } from "../components/ItemsList";
import Pagination from "../Pagination/Pagination";
import Header from "../components/Header";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { ListUsageRequest, Ordering, Usage, WorkspaceInstanceUsageData } from "@gitpod/gitpod-protocol/lib/usage";
import dayjs from "dayjs";
import { forwardRef, useEffect, useMemo, useState } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { useLocation } from "react-router";
import Header from "../components/Header";
import { Item, ItemField, ItemsList } from "../components/ItemsList";
import { useListUsage } from "../data/usage/usage-query";
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";
import Spinner from "../icons/Spinner.svg";
import { ReactComponent as UsageIcon } from "../images/usage-default.svg";
import Pagination from "../Pagination/Pagination";
import { toRemoteURL } from "../projects/render-utils";
import { WorkspaceType } from "@gitpod/gitpod-protocol";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { gitpodHostUrl } from "../service/service";
import "./react-datepicker.css";
import { useLocation } from "react-router";
import dayjs from "dayjs";
import { Heading1, Heading2, Subheading } from "./typography/headings";
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";

interface UsageViewProps {
attributionId: AttributionId;
}

function UsageView({ attributionId }: UsageViewProps) {
const [usagePage, setUsagePage] = useState<ListUsageResponse | undefined>(undefined);
const [page, setPage] = useState(1);
const [errorMessage, setErrorMessage] = useState("");
const startOfCurrentMonth = dayjs().startOf("month");
const [startDate, setStartDate] = useState(startOfCurrentMonth);
const [endDate, setEndDate] = useState(dayjs());
const [totalCreditsUsed, setTotalCreditsUsed] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const supportedClasses = useWorkspaceClasses();

const location = useLocation();
useEffect(() => {
const match = /#(\d{4}-\d{2}-\d{2}):(\d{4}-\d{2}-\d{2})/.exec(location.hash);
Expand All @@ -56,39 +48,29 @@ function UsageView({ attributionId }: UsageViewProps) {
}
}
}, [location]);
const request = useMemo(() => {
const request: ListUsageRequest = {
attributionId: AttributionId.render(attributionId),
from: startDate.startOf("day").valueOf(),
to: endDate.endOf("day").valueOf(),
order: Ordering.ORDERING_DESCENDING,
pagination: {
perPage: 50,
page,
},
};
return request;
}, [attributionId, endDate, page, startDate]);
const usagePage = useListUsage(request);

if (usagePage.error) {
if ((usagePage.error as any).code === ErrorCodes.PERMISSION_DENIED) {
setErrorMessage("Access to usage details is restricted to team owners.");
} else {
setErrorMessage(`Error: ${usagePage.error?.message}`);
}
}

const loadPage = useCallback(
async (page: number = 1) => {
if (usagePage === undefined) {
setIsLoading(true);
setTotalCreditsUsed(0);
}
const request: ListUsageRequest = {
attributionId: AttributionId.render(attributionId),
from: startDate.startOf("day").valueOf(),
to: endDate.endOf("day").valueOf(),
order: Ordering.ORDERING_DESCENDING,
pagination: {
perPage: 50,
page,
},
};
try {
const page = await getGitpodService().server.listUsage(request);
setUsagePage(page);
setTotalCreditsUsed(page.creditsUsed);
} 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);
}
},
[attributionId, endDate, startDate, usagePage],
);
useEffect(() => {
if (startDate.isAfter(endDate)) {
setErrorMessage("The start date needs to be before the end date.");
Expand All @@ -99,8 +81,8 @@ function UsageView({ attributionId }: UsageViewProps) {
return;
}
setErrorMessage("");
loadPage(1);
}, [startDate, endDate, loadPage]);
setPage(1);
}, [startDate, endDate, setPage]);

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

const currentPaginatedResults = usagePage?.usageEntriesList.filter((u) => u.kind === "workspaceinstance") ?? [];
const currentPaginatedResults =
usagePage.data?.usageEntriesList.filter((u) => u.kind === "workspaceinstance") ?? [];
const DateDisplay = forwardRef((arg: any, ref: any) => (
<div
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"
Expand Down Expand Up @@ -253,21 +236,21 @@ function UsageView({ attributionId }: UsageViewProps) {
<div className="text-base text-gray-500 truncate">Previous Months</div>
{getBillingHistory()}
</div>
{!isLoading && (
{!usagePage.isLoading && (
<div>
<div className="flex flex-col truncate">
<div className="text-base text-gray-500">Credits</div>
<div className="flex text-lg text-gray-600 font-semibold">
<span className="dark:text-gray-400">
{totalCreditsUsed.toLocaleString()}
{usagePage.data?.creditsUsed.toLocaleString()}
</span>
</div>
</div>
</div>
)}
</div>
</div>
{!isLoading &&
{!usagePage.isLoading &&
(usagePage === undefined || currentPaginatedResults.length === 0) &&
!errorMessage && (
<div className="flex flex-col w-full mb-8">
Expand All @@ -282,13 +265,13 @@ function UsageView({ attributionId }: UsageViewProps) {
</Subheading>
</div>
)}
{isLoading && (
{usagePage.isLoading && (
<div className="flex items-center justify-center w-full space-x-2 text-gray-400 text-sm pt-16 pb-40">
<img alt="Loading Spinner" className="h-4 w-4 animate-spin" src={Spinner} />
<span>Fetching usage...</span>
</div>
)}
{!isLoading && currentPaginatedResults.length > 0 && (
{!usagePage.isLoading && currentPaginatedResults.length > 0 && (
<div className="flex flex-col w-full mb-8">
<ItemsList className="mt-2 text-gray-400 dark:text-gray-500">
<Item
Expand Down Expand Up @@ -402,13 +385,15 @@ function UsageView({ attributionId }: UsageViewProps) {
);
})}
</ItemsList>
{usagePage && usagePage.pagination && usagePage.pagination.totalPages > 1 && (
<Pagination
currentPage={usagePage.pagination.page}
setPage={(page) => loadPage(page)}
totalNumberOfPages={usagePage.pagination.totalPages}
/>
)}
{usagePage.data &&
usagePage.data.pagination &&
usagePage.data.pagination.totalPages > 1 && (
<Pagination
currentPage={usagePage.data.pagination.page}
setPage={setPage}
totalNumberOfPages={usagePage.data.pagination.totalPages}
/>
)}
</div>
)}
</div>
Expand Down
28 changes: 28 additions & 0 deletions components/dashboard/src/data/usage/usage-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
import { useQuery } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";

export function useListUsage(request?: ListUsageRequest) {
const query = useQuery<ListUsageResponse, Error>(
["usage", request],
() => {
console.log("Fetching usage... ", request);
if (!request) {
throw new Error("request is required");
}
return getGitpodService().server.listUsage(request);
},
{
enabled: !!request,
cacheTime: 1000 * 60 * 10, // 10 minutes
staleTime: 1000 * 60 * 10, // 10 minutes
},
);
return query;
}