Path: blob/main/components/dashboard/src/usage/UsageView.tsx
2499 views
/**1* Copyright (c) 2022 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";7import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";8import { Ordering } from "@gitpod/gitpod-protocol/lib/usage";9import dayjs, { Dayjs } from "dayjs";10import { FC, useCallback, useMemo } from "react";11import { useHistory, useLocation } from "react-router";12import Header from "../components/Header";13import { Item, ItemField, ItemsList } from "../components/ItemsList";14import { useListUsage } from "../data/usage/usage-query";15import Spinner from "../icons/Spinner.svg";16import Pagination from "../Pagination/Pagination";17import { gitpodHostUrl } from "../service/service";18import { Heading2, Subheading } from "../components/typography/headings";19import { UsageSummaryData } from "./UsageSummary";20import { UsageEntry } from "./UsageEntry";21import Alert from "../components/Alert";22import classNames from "classnames";23import { UsageDateFilters } from "./UsageDateFilters";24import { DownloadUsage } from "./download/DownloadUsage";25import { useQueryParams } from "../hooks/use-query-params";2627const DATE_PARAM_FORMAT = "YYYY-MM-DD";2829interface UsageViewProps {30attributionId: AttributionId;31}3233export const UsageView: FC<UsageViewProps> = ({ attributionId }) => {34const location = useLocation();35const history = useHistory();36const params = useQueryParams();3738// page filter params are all in the url as querystring params39const startOfCurrentMonth = dayjs().startOf("month");40const startDate = getDateFromParam(params.get("start")) || startOfCurrentMonth;41const endDate = getDateFromParam(params.get("end")) || dayjs();42const page = getNumberFromParam(params.get("page")) || 1;4344const usagePage = useListUsage({45attributionId: AttributionId.render(attributionId),46from: startDate.startOf("day").valueOf(),47to: endDate.endOf("day").valueOf(),48order: Ordering.ORDERING_DESCENDING,49pagination: {50perPage: 50,51page,52},53});5455// Updates the query params w/ new values overlaid onto existing values56const updatePageParams = useCallback(57(pageParams: { start?: Dayjs; end?: Dayjs; page?: number }) => {58const newParams = new URLSearchParams(params);59newParams.set("start", (pageParams.start || startDate).format(DATE_PARAM_FORMAT));60newParams.set("end", (pageParams.end || endDate).format(DATE_PARAM_FORMAT));61newParams.set("page", `${pageParams.page || page}`);6263history.push(`${location.pathname}?${newParams}`);64},65[endDate, history, location.pathname, page, params, startDate],66);6768const handlePageChange = useCallback(69(val: number) => {70updatePageParams({ page: val });71},72[updatePageParams],73);7475const errorMessage = useMemo(() => {76let errorMessage = "";7778if (usagePage.error) {79if ((usagePage.error as any).code === ErrorCodes.PERMISSION_DENIED) {80errorMessage = "Access to usage details is restricted to team owners.";81} else {82errorMessage = `${usagePage.error?.message}`;83}84}8586return errorMessage;87}, [usagePage.error]);8889const usageEntries = usagePage.data?.usageEntriesList ?? [];9091const readableSchedulerDuration = useMemo(() => {92const intervalMinutes = usagePage.data?.ledgerIntervalMinutes;93if (!intervalMinutes) {94return "";95}9697return `${intervalMinutes} minute${intervalMinutes !== 1 ? "s" : ""}`;98}, [usagePage.data]);99100return (101<>102<Header103title="Usage"104subtitle={105"Organization usage" +106(readableSchedulerDuration ? ", updated every " + readableSchedulerDuration : "") +107"."108}109/>110<div className="app-container pt-5">111<div112className={classNames(113"flex flex-col items-start space-y-3 justify-between px-3",114"md:flex-row md:items-center md:space-x-4 md:space-y-0",115)}116>117<UsageDateFilters startDate={startDate} endDate={endDate} onDateRangeChange={updatePageParams} />118<DownloadUsage attributionId={attributionId} startDate={startDate} endDate={endDate} />119</div>120121{errorMessage && (122<Alert type="error" className="mt-4">123{errorMessage}124</Alert>125)}126127<UsageSummaryData creditsUsed={usagePage.data?.creditsUsed} />128129<div className="flex flex-col w-full mb-8">130<ItemsList className="mt-2 text-gray-400 dark:text-gray-500">131<Item header={false} className="grid grid-cols-12 gap-x-3 bg-pk-surface-secondary">132<ItemField className="col-span-2 my-auto ">133<span>Type</span>134</ItemField>135<ItemField className="col-span-5 my-auto">136<span>ID</span>137</ItemField>138<ItemField className="my-auto">139<span>Credits</span>140</ItemField>141<ItemField className="my-auto" />142<ItemField className="my-auto">143<span>Timestamp</span>144</ItemField>145</Item>146147{/* results loading */}148{usagePage.isLoading && (149<div className="flex items-center justify-center w-full space-x-2 text-gray-400 text-sm pt-16 pb-40">150<img alt="Loading Spinner" className="h-4 w-4 animate-spin" src={Spinner} />151<span>Loading usage...</span>152</div>153)}154155{/* results */}156{!usagePage.isLoading &&157usageEntries.map((usage) => {158return <UsageEntry key={usage.id} usage={usage} />;159})}160161{/* No results */}162{!usagePage.isLoading && usageEntries.length === 0 && !errorMessage && (163<div className="flex flex-col w-full mb-8">164<Heading2 className="text-center mt-8">No sessions found.</Heading2>165<Subheading className="text-center mt-1">166Have you started any167<a className="gp-link" href={gitpodHostUrl.asWorkspacePage().toString()}>168{" "}169workspaces170</a>{" "}171in {startDate.format("MMMM YYYY")} or checked your other organizations?172</Subheading>173</div>174)}175</ItemsList>176177{usagePage.data && usagePage.data.pagination && usagePage.data.pagination.totalPages > 1 && (178<Pagination179currentPage={usagePage.data.pagination.page}180setPage={handlePageChange}181totalNumberOfPages={usagePage.data.pagination.totalPages}182/>183)}184</div>185</div>186</>187);188};189190const getDateFromParam = (paramValue: string | null) => {191if (!paramValue) {192return null;193}194195try {196const date = dayjs(paramValue, DATE_PARAM_FORMAT, true);197if (!date.isValid()) {198return null;199}200201return date;202} catch (e) {203return null;204}205};206207const getNumberFromParam = (paramValue: string | null) => {208if (!paramValue) {209return null;210}211212try {213const number = Number.parseInt(paramValue, 10);214if (Number.isNaN(number)) {215return null;216}217218return number;219} catch (e) {220return null;221}222};223224225