From 624d61f2e48b8b841472545929402402c7a22f9a Mon Sep 17 00:00:00 2001 From: Sagar Rajput Date: Thu, 6 Jun 2024 02:15:30 +0530 Subject: [PATCH 1/6] feat: added option to download billing usage data as csv --- .../BillingContainer/BillingContainer.tsx | 62 +++++++-- .../BillingUsageGraph/BillingUsageGraph.tsx | 17 ++- .../BillingUsageGraph/generateCsvData.ts | 119 ++++++++++++++++++ .../BillingUsageGraph/utils.ts | 13 +- 4 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index 9b45801356..3e9b45c7b9 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-loop-func */ import './BillingContainer.styles.scss'; -import { CheckCircleOutlined } from '@ant-design/icons'; +import { CheckCircleOutlined, CloudDownloadOutlined } from '@ant-design/icons'; import { Color } from '@signozhq/design-tokens'; import { Alert, @@ -27,6 +27,7 @@ import useAxiosError from 'hooks/useAxiosError'; import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import { isEmpty, pick } from 'lodash-es'; +import { unparse } from 'papaparse'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; @@ -40,6 +41,9 @@ import { isCloudUser } from 'utils/app'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph'; +import generateCsvData, { + QuantityData, +} from './BillingUsageGraph/generateCsvData'; interface DataType { key: string; @@ -132,6 +136,7 @@ export default function BillingContainer(): JSX.Element { const [daysRemaining, setDaysRemaining] = useState(0); const [isFreeTrial, setIsFreeTrial] = useState(false); const [data, setData] = useState([]); + const [csvData, setCsvData] = useState([]); const [apiResponse, setApiResponse] = useState< Partial >({}); @@ -346,10 +351,18 @@ export default function BillingContainer(): JSX.Element { updateCreditCard, ]); + const getCsvData = (quantityData: QuantityData[]): void => { + setCsvData(generateCsvData(quantityData)); + }; + const BillingUsageGraphCallback = useCallback( () => !isLoading && !isFetchingBillingData ? ( - + ) : ( @@ -371,6 +384,17 @@ export default function BillingContainer(): JSX.Element { ); + const handleCsvDownload = useCallback((): void => { + const csv = unparse(csvData); + const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const csvUrl = URL.createObjectURL(csvBlob); + const downloadLink = document.createElement('a'); + downloadLink.href = csvUrl; + downloadLink.download = `test-csv-download.csv`; + downloadLink.click(); + downloadLink.remove(); + }, [csvData]); + return (
@@ -399,17 +423,29 @@ export default function BillingContainer(): JSX.Element { ) : null} - + + + + {licensesData?.payload?.onTrial && diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx index 0aecc5d102..0af7811ec5 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx @@ -12,17 +12,20 @@ import getRenderer from 'lib/uPlotLib/utils/getRenderer'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale'; import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import uPlot from 'uplot'; +import { QuantityData } from './generateCsvData'; import { convertDataToMetricRangePayload, fillMissingValuesForQuantities, + quantityDataArr, } from './utils'; interface BillingUsageGraphProps { data: any; billAmount: number; + getCsvData: (quantityData: QuantityData[]) => void; } const paths = ( u: any, @@ -57,7 +60,7 @@ const calculateStartEndTime = ( }; export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { - const { data, billAmount } = props; + const { data, billAmount, getCsvData } = props; const graphCompatibleData = useMemo( () => convertDataToMetricRangePayload(data), [data], @@ -71,6 +74,16 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { data, ]); + const quantityMapArr = useMemo( + () => quantityDataArr(graphCompatibleData, chartData[0]), + [chartData, graphCompatibleData], + ); + + useEffect(() => { + getCsvData(quantityMapArr); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const getGraphSeries = (color: string, label: string): any => ({ drawStyle: 'bars', paths, diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts new file mode 100644 index 0000000000..3c588fcddd --- /dev/null +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts @@ -0,0 +1,119 @@ +import dayjs from 'dayjs'; + +export interface QuantityData { + metric: string; + values: [number, number][]; + queryName: string; + legend: string; + quantity: number[]; + unit: string; +} + +interface DataPoint { + date: string; + metric: { + total: number; + cost: number; + }; + trace: { + total: number; + cost: number; + }; + log: { + total: number; + cost: number; + }; +} + +const formatDate = (timestamp: number): string => + dayjs.unix(timestamp).format('MM/DD/YYYY'); + +const getQuantityData = ( + data: QuantityData[], + metricName: string, +): QuantityData => { + const defaultData: QuantityData = { + metric: metricName, + values: [], + queryName: metricName, + legend: metricName, + quantity: [], + unit: '', + }; + return data.find((d) => d.metric === metricName) || defaultData; +}; + +const generateCsvData = (quantityData: QuantityData[]): any[] => { + const convertData = (data: QuantityData[]): DataPoint[] => { + const metricsData = getQuantityData(data, 'Metrics'); + const tracesData = getQuantityData(data, 'Traces'); + const logsData = getQuantityData(data, 'Logs'); + + const timestamps = metricsData.values.map((value) => value[0]); + + return timestamps.map((timestamp, index) => { + const date = formatDate(timestamp); + + return { + date, + metric: { + total: metricsData.quantity[index] ?? 0, + cost: metricsData.values[index]?.[1] ?? 0, + }, + trace: { + total: tracesData.quantity[index] ?? 0, + cost: tracesData.values[index]?.[1] ?? 0, + }, + log: { + total: logsData.quantity[index] ?? 0, + cost: logsData.values[index]?.[1] ?? 0, + }, + }; + }); + }; + + const formattedData = convertData(quantityData); + + // Calculate totals + const totals = formattedData.reduce( + (acc, dataPoint) => { + acc.metric.total += dataPoint.metric.total; + acc.metric.cost += dataPoint.metric.cost; + acc.trace.total += dataPoint.trace.total; + acc.trace.cost += dataPoint.trace.cost; + acc.log.total += dataPoint.log.total; + acc.log.cost += dataPoint.log.cost; + return acc; + }, + { + metric: { total: 0, cost: 0 }, + trace: { total: 0, cost: 0 }, + log: { total: 0, cost: 0 }, + }, + ); + + const csvData = formattedData.map((dataPoint) => ({ + Date: dataPoint.date, + 'Metrics Vol (Mn samples)': dataPoint.metric.total, + 'Metrics Cost ($)': dataPoint.metric.cost, + 'Traces Vol (GBs)': dataPoint.trace.total, + 'Traces Cost ($)': dataPoint.trace.cost, + 'Logs Vol (GBs)': dataPoint.log.total, + 'Logs Cost ($)': dataPoint.log.cost, + })); + + // Add totals row + csvData.push({ + Date: 'Total', + 'Metrics Vol (Mn samples)': totals.metric.total, + 'Metrics Cost ($)': totals.metric.cost, + 'Traces Vol (GBs)': totals.trace.total, + 'Traces Cost ($)': totals.trace.cost, + 'Logs Vol (GBs)': totals.log.total, + 'Logs Cost ($)': totals.log.cost, + }); + + return csvData; +}; + +export default generateCsvData; diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts index d40c8a6097..1a37e8f0fd 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts @@ -58,10 +58,7 @@ export const convertDataToMetricRangePayload = ( }; }; -export function fillMissingValuesForQuantities( - data: any, - timestampArray: number[], -): MetricRangePayloadProps { +export function quantityDataArr(data: any, timestampArray: number[]): any[] { const { result } = data.data; const transformedResultArr: any[] = []; @@ -76,6 +73,14 @@ export function fillMissingValuesForQuantities( ); transformedResultArr.push({ ...item, quantity: quantityArray }); }); + return transformedResultArr; +} + +export function fillMissingValuesForQuantities( + data: any, + timestampArray: number[], +): MetricRangePayloadProps { + const transformedResultArr = quantityDataArr(data, timestampArray); return { data: { From b02713071cd9359ed87f948f56ec2e0ee1932af3 Mon Sep 17 00:00:00 2001 From: Sagar Rajput Date: Thu, 6 Jun 2024 09:47:20 +0530 Subject: [PATCH 2/6] feat: rounded off values to 2 decimal places --- .../BillingUsageGraph/generateCsvData.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts index 3c588fcddd..b70526aaa8 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts @@ -25,6 +25,16 @@ interface DataPoint { }; } +interface CsvData { + Date: string; + 'Metrics Vol (Mn samples)': number; + 'Metrics Cost ($)': number; + 'Traces Vol (GBs)': number; + 'Traces Cost ($)': number; + 'Logs Vol (GBs)': number; + 'Logs Cost ($)': number; +} + const formatDate = (timestamp: number): string => dayjs.unix(timestamp).format('MM/DD/YYYY'); @@ -92,25 +102,25 @@ const generateCsvData = (quantityData: QuantityData[]): any[] => { }, ); - const csvData = formattedData.map((dataPoint) => ({ + const csvData: CsvData[] = formattedData.map((dataPoint) => ({ Date: dataPoint.date, - 'Metrics Vol (Mn samples)': dataPoint.metric.total, - 'Metrics Cost ($)': dataPoint.metric.cost, - 'Traces Vol (GBs)': dataPoint.trace.total, - 'Traces Cost ($)': dataPoint.trace.cost, - 'Logs Vol (GBs)': dataPoint.log.total, - 'Logs Cost ($)': dataPoint.log.cost, + 'Metrics Vol (Mn samples)': parseFloat(dataPoint.metric.total.toFixed(2)), + 'Metrics Cost ($)': parseFloat(dataPoint.metric.cost.toFixed(2)), + 'Traces Vol (GBs)': parseFloat(dataPoint.trace.total.toFixed(2)), + 'Traces Cost ($)': parseFloat(dataPoint.trace.cost.toFixed(2)), + 'Logs Vol (GBs)': parseFloat(dataPoint.log.total.toFixed(2)), + 'Logs Cost ($)': parseFloat(dataPoint.log.cost.toFixed(2)), })); // Add totals row csvData.push({ Date: 'Total', - 'Metrics Vol (Mn samples)': totals.metric.total, - 'Metrics Cost ($)': totals.metric.cost, - 'Traces Vol (GBs)': totals.trace.total, - 'Traces Cost ($)': totals.trace.cost, - 'Logs Vol (GBs)': totals.log.total, - 'Logs Cost ($)': totals.log.cost, + 'Metrics Vol (Mn samples)': parseFloat(totals.metric.total.toFixed(2)), + 'Metrics Cost ($)': parseFloat(totals.metric.cost.toFixed(2)), + 'Traces Vol (GBs)': parseFloat(totals.trace.total.toFixed(2)), + 'Traces Cost ($)': parseFloat(totals.trace.cost.toFixed(2)), + 'Logs Vol (GBs)': parseFloat(totals.log.total.toFixed(2)), + 'Logs Cost ($)': parseFloat(totals.log.cost.toFixed(2)), }); return csvData; From e777cb85b2a8cd4c6a05edc59e9b3fca48d9a2fa Mon Sep 17 00:00:00 2001 From: Sagar Rajput Date: Thu, 6 Jun 2024 16:19:40 +0530 Subject: [PATCH 3/6] feat: removed state and use useRef --- .../container/BillingContainer/BillingContainer.tsx | 12 ++++-------- .../BillingUsageGraph/BillingUsageGraph.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index 3e9b45c7b9..67d2925f74 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -28,7 +28,7 @@ import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import { isEmpty, pick } from 'lodash-es'; import { unparse } from 'papaparse'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; import { useSelector } from 'react-redux'; @@ -136,7 +136,7 @@ export default function BillingContainer(): JSX.Element { const [daysRemaining, setDaysRemaining] = useState(0); const [isFreeTrial, setIsFreeTrial] = useState(false); const [data, setData] = useState([]); - const [csvData, setCsvData] = useState([]); + const csvData = useRef([]); const [apiResponse, setApiResponse] = useState< Partial >({}); @@ -351,17 +351,13 @@ export default function BillingContainer(): JSX.Element { updateCreditCard, ]); - const getCsvData = (quantityData: QuantityData[]): void => { - setCsvData(generateCsvData(quantityData)); - }; - const BillingUsageGraphCallback = useCallback( () => !isLoading && !isFetchingBillingData ? ( ) : ( @@ -385,7 +381,7 @@ export default function BillingContainer(): JSX.Element { ); const handleCsvDownload = useCallback((): void => { - const csv = unparse(csvData); + const csv = unparse(generateCsvData(csvData.current)); const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const csvUrl = URL.createObjectURL(csvBlob); const downloadLink = document.createElement('a'); diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx index 0af7811ec5..be2b8783d9 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx @@ -12,7 +12,7 @@ import getRenderer from 'lib/uPlotLib/utils/getRenderer'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale'; import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale'; -import { useEffect, useMemo, useRef } from 'react'; +import { MutableRefObject, useEffect, useMemo, useRef } from 'react'; import uPlot from 'uplot'; import { QuantityData } from './generateCsvData'; @@ -25,7 +25,7 @@ import { interface BillingUsageGraphProps { data: any; billAmount: number; - getCsvData: (quantityData: QuantityData[]) => void; + csvData: MutableRefObject; } const paths = ( u: any, @@ -60,7 +60,7 @@ const calculateStartEndTime = ( }; export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { - const { data, billAmount, getCsvData } = props; + const { data, billAmount, csvData } = props; const graphCompatibleData = useMemo( () => convertDataToMetricRangePayload(data), [data], @@ -80,7 +80,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { ); useEffect(() => { - getCsvData(quantityMapArr); + csvData.current = quantityMapArr; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 4a6f4a4594045bb53941231944eb0f32ae8d6531 Mon Sep 17 00:00:00 2001 From: Sagar Rajput Date: Fri, 7 Jun 2024 18:14:45 +0530 Subject: [PATCH 4/6] feat: removed ref and added a function handler for csv --- .../BillingContainer/BillingContainer.tsx | 22 ++++------- .../BillingUsageGraph/BillingUsageGraph.tsx | 17 +------- .../BillingUsageGraph/utils.ts | 39 +++++++++++++++++++ 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index 67d2925f74..c9fc43c4c0 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -27,8 +27,7 @@ import useAxiosError from 'hooks/useAxiosError'; import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import { isEmpty, pick } from 'lodash-es'; -import { unparse } from 'papaparse'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; import { useSelector } from 'react-redux'; @@ -41,9 +40,7 @@ import { isCloudUser } from 'utils/app'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph'; -import generateCsvData, { - QuantityData, -} from './BillingUsageGraph/generateCsvData'; +import { prepareCsvData } from './BillingUsageGraph/utils'; interface DataType { key: string; @@ -136,7 +133,6 @@ export default function BillingContainer(): JSX.Element { const [daysRemaining, setDaysRemaining] = useState(0); const [isFreeTrial, setIsFreeTrial] = useState(false); const [data, setData] = useState([]); - const csvData = useRef([]); const [apiResponse, setApiResponse] = useState< Partial >({}); @@ -354,11 +350,7 @@ export default function BillingContainer(): JSX.Element { const BillingUsageGraphCallback = useCallback( () => !isLoading && !isFetchingBillingData ? ( - + ) : ( @@ -381,15 +373,15 @@ export default function BillingContainer(): JSX.Element { ); const handleCsvDownload = useCallback((): void => { - const csv = unparse(generateCsvData(csvData.current)); - const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const csv = prepareCsvData(apiResponse); + const csvBlob = new Blob([csv.csvData], { type: 'text/csv;charset=utf-8;' }); const csvUrl = URL.createObjectURL(csvBlob); const downloadLink = document.createElement('a'); downloadLink.href = csvUrl; - downloadLink.download = `test-csv-download.csv`; + downloadLink.download = csv.fileName; downloadLink.click(); downloadLink.remove(); - }, [csvData]); + }, [apiResponse]); return (
diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx index be2b8783d9..0aecc5d102 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx @@ -12,20 +12,17 @@ import getRenderer from 'lib/uPlotLib/utils/getRenderer'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale'; import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale'; -import { MutableRefObject, useEffect, useMemo, useRef } from 'react'; +import { useMemo, useRef } from 'react'; import uPlot from 'uplot'; -import { QuantityData } from './generateCsvData'; import { convertDataToMetricRangePayload, fillMissingValuesForQuantities, - quantityDataArr, } from './utils'; interface BillingUsageGraphProps { data: any; billAmount: number; - csvData: MutableRefObject; } const paths = ( u: any, @@ -60,7 +57,7 @@ const calculateStartEndTime = ( }; export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { - const { data, billAmount, csvData } = props; + const { data, billAmount } = props; const graphCompatibleData = useMemo( () => convertDataToMetricRangePayload(data), [data], @@ -74,16 +71,6 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element { data, ]); - const quantityMapArr = useMemo( - () => quantityDataArr(graphCompatibleData, chartData[0]), - [chartData, graphCompatibleData], - ); - - useEffect(() => { - csvData.current = quantityMapArr; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const getGraphSeries = (color: string, label: string): any => ({ drawStyle: 'bars', paths, diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts index 1a37e8f0fd..5123d59329 100644 --- a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts +++ b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts @@ -1,6 +1,12 @@ +import { UsageResponsePayloadProps } from 'api/billing/getUsage'; +import dayjs from 'dayjs'; +import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { isEmpty, isNull } from 'lodash-es'; +import { unparse } from 'papaparse'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import generateCsvData, { QuantityData } from './generateCsvData'; + export const convertDataToMetricRangePayload = ( data: any, ): MetricRangePayloadProps => { @@ -90,3 +96,36 @@ export function fillMissingValuesForQuantities( }, }; } + +const formatDate = (timestamp: number): string => + dayjs.unix(timestamp).format('MM/DD/YYYY'); + +export function csvFileName(csvData: QuantityData[]): string { + if (!csvData.length) { + return `billing-usage.csv`; + } + + const { values } = csvData[0]; + + const timestamps = values.map((item) => item[0]); + const startDate = formatDate(Math.min(...timestamps)); + const endDate = formatDate(Math.max(...timestamps)); + + return `billing_usage_(${startDate}-${endDate}).csv`; +} + +export function prepareCsvData( + data: Partial, +): { + csvData: string; + fileName: string; +} { + const graphCompatibleData = convertDataToMetricRangePayload(data); + const chartData = getUPlotChartData(graphCompatibleData); + const quantityMapArr = quantityDataArr(graphCompatibleData, chartData[0]); + + return { + csvData: unparse(generateCsvData(quantityMapArr)), + fileName: csvFileName(quantityMapArr), + }; +} From b8d2b5802632f7d92cfd5e6ddf3a33062b4555be Mon Sep 17 00:00:00 2001 From: Sagar Rajput Date: Mon, 10 Jun 2024 11:00:31 +0530 Subject: [PATCH 5/6] feat: added try-catch logic for handleCsvDownload function --- .../BillingContainer/BillingContainer.tsx | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index c9fc43c4c0..7e1d22953a 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -373,15 +373,32 @@ export default function BillingContainer(): JSX.Element { ); const handleCsvDownload = useCallback((): void => { - const csv = prepareCsvData(apiResponse); - const csvBlob = new Blob([csv.csvData], { type: 'text/csv;charset=utf-8;' }); - const csvUrl = URL.createObjectURL(csvBlob); - const downloadLink = document.createElement('a'); - downloadLink.href = csvUrl; - downloadLink.download = csv.fileName; - downloadLink.click(); - downloadLink.remove(); - }, [apiResponse]); + try { + const csv = prepareCsvData(apiResponse); + + if (!csv.csvData || !csv.fileName) { + throw new Error('Invalid CSV data or file name.'); + } + + const csvBlob = new Blob([csv.csvData], { type: 'text/csv;charset=utf-8;' }); + const csvUrl = URL.createObjectURL(csvBlob); + const downloadLink = document.createElement('a'); + + downloadLink.href = csvUrl; + downloadLink.download = csv.fileName; + document.body.appendChild(downloadLink); // Required for Firefox + downloadLink.click(); + + // Clean up + downloadLink.remove(); + URL.revokeObjectURL(csvUrl); // Release the memory associated with the object URL + } catch (error) { + console.error('Error downloading the CSV file:', error); + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + } + }, [apiResponse, notifications]); return (
From 72a724e40dd67e94086d0d40956b30ce05b05ee9 Mon Sep 17 00:00:00 2001 From: Sagar Rajput Date: Mon, 10 Jun 2024 11:02:20 +0530 Subject: [PATCH 6/6] feat: added successful notification --- frontend/src/container/BillingContainer/BillingContainer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index 7e1d22953a..248819723c 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -392,6 +392,9 @@ export default function BillingContainer(): JSX.Element { // Clean up downloadLink.remove(); URL.revokeObjectURL(csvUrl); // Release the memory associated with the object URL + notifications.success({ + message: 'Download successful', + }); } catch (error) { console.error('Error downloading the CSV file:', error); notifications.error({