mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-17 09:15:54 +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 */
|
/* eslint-disable @typescript-eslint/no-loop-func */
|
||||||
import './BillingContainer.styles.scss';
|
import './BillingContainer.styles.scss';
|
||||||
|
|
||||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, CloudDownloadOutlined } from '@ant-design/icons';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@ -40,6 +40,7 @@ import { isCloudUser } from 'utils/app';
|
|||||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||||
|
|
||||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||||
|
import { prepareCsvData } from './BillingUsageGraph/utils';
|
||||||
|
|
||||||
interface DataType {
|
interface DataType {
|
||||||
key: string;
|
key: string;
|
||||||
@ -371,6 +372,37 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
</Typography>
|
</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 (
|
return (
|
||||||
<div className="billing-container">
|
<div className="billing-container">
|
||||||
<Flex vertical style={{ marginBottom: 16 }}>
|
<Flex vertical style={{ marginBottom: 16 }}>
|
||||||
@ -399,17 +431,29 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
<Flex gap={20}>
|
||||||
type="primary"
|
<Button
|
||||||
size="middle"
|
type="dashed"
|
||||||
loading={isLoadingBilling || isLoadingManageBilling}
|
size="middle"
|
||||||
disabled={isLoading}
|
loading={isLoadingBilling || isLoadingManageBilling}
|
||||||
onClick={handleBilling}
|
disabled={isLoading || isFetchingBillingData}
|
||||||
>
|
onClick={handleCsvDownload}
|
||||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
icon={<CloudDownloadOutlined />}
|
||||||
? t('upgrade_plan')
|
>
|
||||||
: t('manage_billing')}
|
Download CSV
|
||||||
</Button>
|
</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>
|
</Flex>
|
||||||
|
|
||||||
{licensesData?.payload?.onTrial &&
|
{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 { isEmpty, isNull } from 'lodash-es';
|
||||||
|
import { unparse } from 'papaparse';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import generateCsvData, { QuantityData } from './generateCsvData';
|
||||||
|
|
||||||
export const convertDataToMetricRangePayload = (
|
export const convertDataToMetricRangePayload = (
|
||||||
data: any,
|
data: any,
|
||||||
): MetricRangePayloadProps => {
|
): MetricRangePayloadProps => {
|
||||||
@ -58,10 +64,7 @@ export const convertDataToMetricRangePayload = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fillMissingValuesForQuantities(
|
export function quantityDataArr(data: any, timestampArray: number[]): any[] {
|
||||||
data: any,
|
|
||||||
timestampArray: number[],
|
|
||||||
): MetricRangePayloadProps {
|
|
||||||
const { result } = data.data;
|
const { result } = data.data;
|
||||||
|
|
||||||
const transformedResultArr: any[] = [];
|
const transformedResultArr: any[] = [];
|
||||||
@ -76,6 +79,14 @@ export function fillMissingValuesForQuantities(
|
|||||||
);
|
);
|
||||||
transformedResultArr.push({ ...item, quantity: quantityArray });
|
transformedResultArr.push({ ...item, quantity: quantityArray });
|
||||||
});
|
});
|
||||||
|
return transformedResultArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillMissingValuesForQuantities(
|
||||||
|
data: any,
|
||||||
|
timestampArray: number[],
|
||||||
|
): MetricRangePayloadProps {
|
||||||
|
const transformedResultArr = quantityDataArr(data, timestampArray);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
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