{!isLoading && (
@@ -403,7 +412,7 @@ export default function BillingContainer(): JSX.Element {
columns={columns}
dataSource={data}
pagination={false}
- summary={renderSummary}
+ bordered={false}
/>
)}
diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.styles.scss b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.styles.scss
new file mode 100644
index 0000000000..e5722d4f4a
--- /dev/null
+++ b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.styles.scss
@@ -0,0 +1,29 @@
+.billing-graph-card {
+ .ant-card-body {
+ height: 40vh;
+ .uplot-graph-container {
+ padding: 8px;
+ }
+ }
+ .total-spent {
+ font-family: 'SF Mono' monospace;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 24px;
+ }
+
+ .total-spent-title {
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 22px;
+ letter-spacing: 0.48px;
+ color: rgba(255, 255, 255, 0.5);
+ }
+}
+
+.lightMode {
+ .total-spent-title {
+ color: var(--bg-ink-100);
+ }
+}
diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx
new file mode 100644
index 0000000000..fcbba624e3
--- /dev/null
+++ b/frontend/src/container/BillingContainer/BillingUsageGraph/BillingUsageGraph.tsx
@@ -0,0 +1,190 @@
+import './BillingUsageGraph.styles.scss';
+import '../../../lib/uPlotLib/uPlotLib.styles.scss';
+
+import { Color } from '@signozhq/design-tokens';
+import { Card, Flex, Typography } from 'antd';
+import { getComponentForPanelType } from 'constants/panelTypes';
+import { PANEL_TYPES } from 'constants/queryBuilder';
+import { PropsTypePropsMap } from 'container/GridPanelSwitch/types';
+import { useIsDarkMode } from 'hooks/useDarkMode';
+import { useResizeObserver } from 'hooks/useDimensions';
+import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
+import getAxes from 'lib/uPlotLib/utils/getAxes';
+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 { FC, useMemo, useRef } from 'react';
+import uPlot from 'uplot';
+
+import {
+ convertDataToMetricRangePayload,
+ fillMissingValuesForQuantities,
+} from './utils';
+
+interface BillingUsageGraphProps {
+ data: any;
+ billAmount: number;
+}
+const paths = (
+ u: any,
+ seriesIdx: number,
+ idx0: number,
+ idx1: number,
+ extendGap: boolean,
+ buildClip: boolean,
+): uPlot.Series.PathBuilder => {
+ const s = u.series[seriesIdx];
+ const style = s.drawStyle;
+ const interp = s.lineInterpolation;
+
+ const renderer = getRenderer(style, interp);
+
+ return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
+};
+
+export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
+ const { data, billAmount } = props;
+ const graphCompatibleData = useMemo(
+ () => convertDataToMetricRangePayload(data),
+ [data],
+ );
+ const chartData = getUPlotChartData(graphCompatibleData);
+ const graphRef = useRef
(null);
+ const isDarkMode = useIsDarkMode();
+ const containerDimensions = useResizeObserver(graphRef);
+
+ const { billingPeriodStart: startTime, billingPeriodEnd: endTime } = data;
+
+ const Component = getComponentForPanelType(PANEL_TYPES.BAR) as FC<
+ PropsTypePropsMap[PANEL_TYPES]
+ >;
+
+ const getGraphSeries = (color: string, label: string): any => ({
+ drawStyle: 'bars',
+ paths,
+ lineInterpolation: 'spline',
+ show: true,
+ label,
+ fill: color,
+ stroke: color,
+ width: 2,
+ spanGaps: true,
+ points: {
+ size: 5,
+ show: false,
+ stroke: color,
+ },
+ });
+
+ const uPlotSeries: any = useMemo(
+ () => [
+ { label: 'Timestamp', stroke: 'purple' },
+ getGraphSeries(
+ '#DECCBC',
+ graphCompatibleData.data.result[0]?.legend as string,
+ ),
+ getGraphSeries(
+ '#4E74F8',
+ graphCompatibleData.data.result[1]?.legend as string,
+ ),
+ getGraphSeries(
+ '#F24769',
+ graphCompatibleData.data.result[2]?.legend as string,
+ ),
+ ],
+ [graphCompatibleData.data.result],
+ );
+
+ const axesOptions = getAxes(isDarkMode, '');
+
+ const optionsForChart: uPlot.Options = useMemo(
+ () => ({
+ id: 'billing-usage-breakdown',
+ series: uPlotSeries,
+ width: containerDimensions.width,
+ height: containerDimensions.height - 30,
+ axes: [
+ {
+ ...axesOptions[0],
+ grid: {
+ ...axesOptions.grid,
+ show: false,
+ stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
+ },
+ },
+ {
+ ...axesOptions[1],
+ stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
+ },
+ ],
+ scales: {
+ x: {
+ ...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
+ },
+ y: {
+ ...getYAxisScale({
+ series: graphCompatibleData?.data.newResult.data.result,
+ yAxisUnit: '',
+ softMax: null,
+ softMin: null,
+ }),
+ },
+ },
+ legend: {
+ show: true,
+ live: false,
+ isolate: true,
+ },
+ cursor: {
+ lock: false,
+ focus: {
+ prox: 1e6,
+ bias: 1,
+ },
+ },
+ focus: {
+ alpha: 0.3,
+ },
+ padding: [32, 32, 16, 16],
+ plugins: [
+ tooltipPlugin(
+ fillMissingValuesForQuantities(graphCompatibleData, chartData[0]),
+ '',
+ true,
+ ),
+ ],
+ }),
+ [
+ axesOptions,
+ chartData,
+ containerDimensions.height,
+ containerDimensions.width,
+ endTime,
+ graphCompatibleData,
+ isDarkMode,
+ startTime,
+ uPlotSeries,
+ ],
+ );
+
+ const numberFormatter = new Intl.NumberFormat('en-US');
+
+ return (
+
+
+
+
+ TOTAL SPENT
+
+
+ ${numberFormatter.format(billAmount)}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts
new file mode 100644
index 0000000000..d40c8a6097
--- /dev/null
+++ b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts
@@ -0,0 +1,87 @@
+import { isEmpty, isNull } from 'lodash-es';
+import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
+
+export const convertDataToMetricRangePayload = (
+ data: any,
+): MetricRangePayloadProps => {
+ const emptyStateData = {
+ data: {
+ newResult: { data: { result: [], resultType: '' } },
+ result: [],
+ resultType: '',
+ },
+ };
+ if (isEmpty(data)) {
+ return emptyStateData;
+ }
+ const {
+ details: { breakdown = [] },
+ } = data || {};
+
+ if (isNull(breakdown) || breakdown.length === 0) {
+ return emptyStateData;
+ }
+
+ const payload = breakdown.map((info: any) => {
+ const metric = info.type;
+ const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
+ (a: any, b: any) => a.timestamp - b.timestamp,
+ );
+ const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
+ categoryInfo.timestamp,
+ categoryInfo.total,
+ ]);
+ const queryName = info.type;
+ const legend = info.type;
+ const { unit } = info;
+ const quantity = sortedBreakdownData.map(
+ (categoryInfo: any) => categoryInfo.quantity,
+ );
+ return { metric, values, queryName, legend, quantity, unit };
+ });
+
+ const sortedData = payload.sort((a: any, b: any) => {
+ const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
+ const avgA = a.values.length ? sumA / a.values.length : 0;
+ const sumB = b.values.reduce((acc: any, val: any) => acc + val[1], 0);
+ const avgB = b.values.length ? sumB / b.values.length : 0;
+
+ return sumA === sumB ? avgB - avgA : sumB - sumA;
+ });
+
+ return {
+ data: {
+ newResult: { data: { result: sortedData, resultType: '' } },
+ result: sortedData,
+ resultType: '',
+ },
+ };
+};
+
+export function fillMissingValuesForQuantities(
+ data: any,
+ timestampArray: number[],
+): MetricRangePayloadProps {
+ const { result } = data.data;
+
+ const transformedResultArr: any[] = [];
+ result.forEach((item: any) => {
+ const timestampToQuantityMap: { [timestamp: number]: number } = {};
+ item.values.forEach((val: number[], index: number) => {
+ timestampToQuantityMap[val[0]] = item.quantity[index];
+ });
+
+ const quantityArray = timestampArray.map(
+ (timestamp: number) => timestampToQuantityMap[timestamp] ?? null,
+ );
+ transformedResultArr.push({ ...item, quantity: quantityArray });
+ });
+
+ return {
+ data: {
+ newResult: { data: { result: transformedResultArr, resultType: '' } },
+ result: transformedResultArr,
+ resultType: '',
+ },
+ };
+}
diff --git a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
index 3a69f459fe..4ec3677dfb 100644
--- a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
+++ b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
@@ -27,6 +27,7 @@ const generateTooltipContent = (
idx: number,
yAxisUnit?: string,
series?: uPlot.Options['series'],
+ isBillingUsageGraphs?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): HTMLElement => {
const container = document.createElement('div');
@@ -49,12 +50,22 @@ const generateTooltipContent = (
if (Array.isArray(series) && series.length > 0) {
series.forEach((item, index) => {
if (index === 0) {
- tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss');
+ if (isBillingUsageGraphs) {
+ tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY');
+ } else {
+ tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss');
+ }
} else if (item.show) {
- const { metric = {}, queryName = '', legend = '' } =
- seriesList[index - 1] || {};
+ const {
+ metric = {},
+ queryName = '',
+ legend = '',
+ quantity = [],
+ unit = '',
+ } = seriesList[index - 1] || {};
const value = data[index][idx];
+ const dataIngested = quantity[idx];
const label = getLabelName(metric, queryName || '', legend || '');
const color = generateColor(label, themeColors.chartcolors);
@@ -63,6 +74,7 @@ const generateTooltipContent = (
if (Number.isFinite(value)) {
const tooltipValue = getToolTipValue(value, yAxisUnit);
+ const dataIngestedFormated = getToolTipValue(dataIngested);
if (
duplicatedLegendLabels[label] ||
Object.prototype.hasOwnProperty.call(formattedData, label)
@@ -93,7 +105,9 @@ const generateTooltipContent = (
value,
tooltipValue,
queryName,
- textContent: `${tooltipItemLabel} : ${tooltipValue}`,
+ textContent: isBillingUsageGraphs
+ ? `${tooltipItemLabel} : $${tooltipValue} - ${dataIngestedFormated} ${unit}`
+ : `${tooltipItemLabel} : ${tooltipValue}`,
};
tooltipCount += 1;
@@ -168,6 +182,7 @@ const generateTooltipContent = (
const tooltipPlugin = (
apiResponse: MetricRangePayloadProps | undefined,
yAxisUnit?: string,
+ isBillingUsageGraphs?: boolean,
): any => {
let over: HTMLElement;
let bound: HTMLElement;
@@ -228,6 +243,7 @@ const tooltipPlugin = (
idx,
yAxisUnit,
u.series,
+ isBillingUsageGraphs,
);
overlay.appendChild(content);
placement(overlay, anchor, 'right', 'start', { bound });
diff --git a/frontend/src/pages/Billing/BillingPage.styles.scss b/frontend/src/pages/Billing/BillingPage.styles.scss
index ced1d4d055..bb6bd3b529 100644
--- a/frontend/src/pages/Billing/BillingPage.styles.scss
+++ b/frontend/src/pages/Billing/BillingPage.styles.scss
@@ -2,4 +2,6 @@
display: flex;
width: 100%;
color: #fff;
+ justify-content: center;
+ align-items: center;
}
diff --git a/frontend/src/types/api/widgets/getQuery.ts b/frontend/src/types/api/widgets/getQuery.ts
index 0b36af1541..5f455698dd 100644
--- a/frontend/src/types/api/widgets/getQuery.ts
+++ b/frontend/src/types/api/widgets/getQuery.ts
@@ -14,6 +14,8 @@ export interface QueryData {
queryName: string;
legend?: string;
values: [number, string][];
+ quantity?: number[];
+ unit?: string;
}
export interface SeriesItem {
@@ -28,6 +30,9 @@ export interface QueryDataV3 {
queryName: string;
legend?: string;
series: SeriesItem[] | null;
+ quantity?: number;
+ unitPrice?: number;
+ unit?: string;
}
export interface Props {