mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-16 18:25:56 +08:00
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:
parent
699f79d6ba
commit
a319d1ec53
@ -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 &&
|
||||
|
@ -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;
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user