Path: blob/main/components/dashboard/src/usage/download/download-usage-csv.ts
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 { useCallback } from "react";7import dayjs from "dayjs";8import { useQuery, useQueryClient } from "@tanstack/react-query";9import { ListUsageRequest } from "@gitpod/gitpod-protocol/lib/usage";10import { getAllUsageRecords } from "./get-usage-records";11import { transformUsageRecord, UsageCSVRow } from "./transform-usage-record";12import { noPersistence } from "../../data/setup";1314type Args = Pick<ListUsageRequest, "attributionId" | "from" | "to"> & {15orgName: string;16signal?: AbortSignal;17onProgress?: (percentage: number) => void;18};1920export type DownloadUsageCSVResponse = {21blob: Blob | null;22filename: string;23count: number;24};2526const downloadUsageCSV = async ({27attributionId,28from,29to,30orgName,31signal,32onProgress,33}: Args): Promise<DownloadUsageCSVResponse> => {34const start = dayjs(from).format("YYYYMMDD");35const end = dayjs(to).format("YYYYMMDD");36const filename = `gitpod-usage-${orgName}-${start}-${end}.csv`;3738const records = await getAllUsageRecords({39attributionId,40from,41to,42signal,43onProgress,44});4546if (records.length === 0) {47return {48blob: null,49filename,50count: 0,51};52}5354const rows = records.map(transformUsageRecord).filter((r) => !!r);55const fields = Object.keys(rows[0]) as (keyof UsageCSVRow)[];5657// TODO: look into a lib to handle this more robustly58// CSV Rows59const csvRows = rows.map((row) => {60const rowString = fields.map((fieldName) => JSON.stringify(row[fieldName])).join(",");6162return rowString;63});6465// Prepend Header66csvRows.unshift(fields.join(","));6768const blob = new Blob([`\ufeff${csvRows.join("\n")}`], {69type: "text/csv;charset=utf-8",70});7172return {73blob,74filename,75count: rows.length,76};77};7879export const useDownloadUsageCSV = (args: Args) => {80const client = useQueryClient();81const key = getDownloadUsageCSVQueryKey(args);8283const abort = useCallback(() => {84client.removeQueries([key]);85}, [client, key]);8687const query = useQuery<DownloadUsageCSVResponse, Error>(88key,89async ({ signal }) => {90const resp = await downloadUsageCSV({ ...args, signal });9192// Introduce a slight artificial delay when completed to allow progress to finish the transition to 100%93// While this feels a bit odd here instead of in the component, it's much easier to add94// the delay here than track it in react state95// 1000ms because the transition duration is 1000ms96await new Promise((r) => setTimeout(r, 1000));9798return resp;99},100{101retry: false,102cacheTime: 0,103staleTime: 0,104},105);106107return {108...query,109abort,110};111};112113const getDownloadUsageCSVQueryKey = (args: Args) => {114return noPersistence(["usage-export", args]);115};116117118