feat: added option to download billing usage data as csv (#5158)

* feat: added option to download billing usage data as csv

* feat: rounded off values to 2 decimal places

* feat: removed state and use useRef

* feat: removed ref and added a function handler for csv

* feat: added try-catch logic for handleCsvDownload function

* feat: added successful notification
This commit is contained in:
SagarRajput-7 2024-06-11 19:21:33 +05:30 committed by GitHub
parent 699f79d6ba
commit a319d1ec53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 233 additions and 16 deletions

View File

@ -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,
@ -40,6 +40,7 @@ import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
import { prepareCsvData } from './BillingUsageGraph/utils';
interface DataType {
key: string;
@ -371,6 +372,37 @@ export default function BillingContainer(): JSX.Element {
</Typography>
);
const handleCsvDownload = useCallback((): void => {
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
notifications.success({
message: 'Download successful',
});
} catch (error) {
console.error('Error downloading the CSV file:', error);
notifications.error({
message: SOMETHING_WENT_WRONG,
});
}
}, [apiResponse, notifications]);
return (
<div className="billing-container">
<Flex vertical style={{ marginBottom: 16 }}>
@ -399,17 +431,29 @@ export default function BillingContainer(): JSX.Element {
</Typography.Text>
) : null}
</Flex>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? t('upgrade_plan')
: t('manage_billing')}
</Button>
<Flex gap={20}>
<Button
type="dashed"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading || isFetchingBillingData}
onClick={handleCsvDownload}
icon={<CloudDownloadOutlined />}
>
Download CSV
</Button>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? t('upgrade_plan')
: t('manage_billing')}
</Button>
</Flex>
</Flex>
{licensesData?.payload?.onTrial &&

View File

@ -0,0 +1,129 @@
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;
};
}
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');
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: CsvData[] = formattedData.map((dataPoint) => ({
Date: dataPoint.date,
'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)': 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;
};
export default generateCsvData;

View File

@ -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 => {
@ -58,10 +64,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 +79,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: {
@ -85,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<UsageResponsePayloadProps>,
): {
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),
};
}