Path: blob/main/components/dashboard/src/usage/download/DownloadUsage.tsx
2500 views
/**1* Copyright (c) 2023 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 { FC, useCallback, useEffect, useMemo, useState } from "react";7import { useDownloadUsageCSV } from "./download-usage-csv";8import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";9import { Dayjs } from "dayjs";10import { useToast } from "../../components/toasts/Toasts";11import { useCurrentOrg } from "../../data/organizations/orgs-query";12import { ReactComponent as DownloadIcon } from "../../icons/Download.svg";13import { ReactComponent as ExclamationIcon } from "../../images/exclamation.svg";14import { LinkButton } from "../../components/LinkButton";15import { saveAs } from "file-saver";16import prettyBytes from "pretty-bytes";17import { ProgressBar } from "../../components/ProgressBar";18import { useTemporaryState } from "../../hooks/use-temporary-value";19import { Button } from "@podkit/buttons/Button";2021type Props = {22attributionId: AttributionId;23startDate: Dayjs;24endDate: Dayjs;25};26export const DownloadUsage: FC<Props> = ({ attributionId, startDate, endDate }) => {27const { data: org } = useCurrentOrg();28const { toast } = useToast();29// When we start the download, we disable the button for a short time30const [downloadDisabled, setDownloadDisabled] = useTemporaryState(false, 1000);3132const handleDownload = useCallback(async () => {33if (!org) {34return;35}3637setDownloadDisabled(true);38toast(39<DownloadUsageToast40orgName={org?.slug ?? org?.id}41attributionId={attributionId}42startDate={startDate}43endDate={endDate}44/>,45{46autoHide: false,47},48);49}, [attributionId, endDate, org, setDownloadDisabled, startDate, toast]);5051return (52// TODO: Convert this to use an IconButton when we add one to podkit53<Button variant="secondary" onClick={handleDownload} className="gap-1" disabled={downloadDisabled}>54<DownloadIcon />55<span>Export as CSV</span>56</Button>57);58};5960type DownloadUsageToastProps = Props & {61orgName: string;62};63const DownloadUsageToast: FC<DownloadUsageToastProps> = ({ attributionId, endDate, startDate, orgName }) => {64const [progress, setProgress] = useState(0);6566const queryArgs = useMemo(67() => ({68orgName,69attributionId: AttributionId.render(attributionId),70from: startDate.startOf("day").valueOf(),71to: endDate.endOf("day").valueOf(),72onProgress: setProgress,73}),74[attributionId, endDate, orgName, startDate],75);76const { data, error, isLoading, abort, remove } = useDownloadUsageCSV(queryArgs);7778const saveFile = useCallback(() => {79if (!data || !data.blob) {80return;81}8283saveAs(data.blob, data.filename);84}, [data]);8586useEffect(() => {87return () => {88abort();89remove();90};91// eslint-disable-next-line react-hooks/exhaustive-deps92}, []);9394if (isLoading) {95return (96<div>97<span>Preparing usage export</span>98<ProgressBar inverted value={progress} />99</div>100);101}102103if (error) {104return (105<div className="flex flex-row items-start space-x-2">106<ExclamationIcon className="w-5 h-5 mt-0.5" />107<div>108<span>Error exporting your usage data:</span>109<pre className="mt-2 whitespace-normal text-sm">{error.message}</pre>110</div>111</div>112);113}114115if (!data || !data.blob || data.count === 0) {116return <span>No usage data for the selected period.</span>;117}118119const readableSize = prettyBytes(data.blob.size);120const formattedCount = Intl.NumberFormat().format(data.count);121122return (123<div className="flex flex-row items-start justify-between space-x-2">124<div>125<span>Usage export complete.</span>126<p className="text-pk-content-invert-primary/90">127{readableSize} · {formattedCount} {data.count !== 1 ? "entries" : "entry"} exported128</p>129</div>130<div>131<LinkButton inverted onClick={saveFile} className="text-left text-base">132Download CSV133</LinkButton>134</div>135</div>136);137};138139140